Posts Tagged ‘interface’

How to deal with interfaces

26/12/2012

Creation

Interfaces (in the sense of the implementation-deprived classes) are the bread and butter of most OO applications out there. They define the contract between client and server.

If I were to give you 4 tips regarding design of interfaces, these would be:

  • Interface should make it easy to do good things, and hard – wrong ones.
  • Prototype your interfaces – test how the interface really copes with the problems at hand.
  • Don’t throw too many eggs into one basket – just because several classes don’t adhere to some interface, doesn’t mean you shouldn’t implement it. Just put the outsiders into their own baskets.
  • Hide implementation. Strive to catch the general, abstracted idea. This way adding new implementers will be easy.

Alright, so you have your sexy interface that looks more of less like the thing beneath:

class ICommunicator {
public:
	virtual ~ICommunicator() {}
	virtual void connect() = 0;
	virtual void* getData() = 0;
};

class TcpClient : public ICommunicator {
public:
	virtual void connect() {
		// ...
	}
        
	virtual void* getData() {
		// ...
	}
};

The idea is rather simple:

  1. Client creates the instance of ICommunicator.
  2. Client connects to some station.
  3. If the connection succeeds (no exception has been thrown) client starts receiving data.

Change

One day, your boss comes to your office (ok, let’s pretend you don’t have to visit him yourself) and asks you to add functionality to encrypt the transmission. Great, you think, it shouldn’t be that hard.

Unfortunately in order for the encryption to work (in our case) you have to follow these steps:

  1. Fetch the initial packet of unencrypted data.
  2. Get info regarding client to which you will be connecting to.
  3. Basing on the client type, specify the certificate.
  4. Switch to a new encrypted socket with a predetermined certificate.
  5. Fetch data as usual.

Now, how you gonna do it? There are several conflicting purposes:

  • small or no change to the interface,
  • low amount of work,
  • extensibility for future changes,
  • cohesiveness of code,
  • high performance,
  • readability & maintainability,

It’s virtually impossible to cover all of them so you will have to decide yourself. For the purpose of this post I will just deal with changing the interface.

Can I make the change?

Before you even start thinking about changing the interface, then you better check if you are allowed to do it. There may be thousands of developers dependent on your code out there, and making any change will definitely break their code. Some companies even prohibit changes to the interface and force you to create always a new one with version number. In other words, when changing IFoo you will create IFooV1.

Does it fit here?

Ask yourself: do I want every implementor of the interface to support encryption? Is this an integral part of the interface that I missed during design?

If we could hide the whole handshaking/certificate swapping behind the connect then no changes should be made. It would be quite reasonable in fact (and I would chose this solution), but due to various constraints we won’t be able to neatly hide it.

The question remains: is encryption a part of requirements? Can every current implementor provide a reasonable way of encrypting the channel? I am not so sure. It is quite a heavy burden placed on the implementors and because of many different ways of implementing the encryption even further changes to the interface could be required.

Can I catch the general idea of interface better?

If you know you are gonna change the interface, maybe it’s time for some refactorization? I mean, is there a way of decomposing interface (i.e., splitting methods into ‘n’ children) so that every implementer will have something meaningful to do in every new method?

Example!

class IFoo {
public:
	virtual ~IFoo() {}
	virtual void doStuff() = 0;
};
// Decomposed
class IFoo {
public:
	virtual ~IFoo() {}
	virtual void initialize() = 0;
	virtual void execute() = 0;
	virtual void finish() = 0;
};

Such decomposition may not be applicable in our case, but I’ve seen quite a few code samples where such change (together with some parameters/return values) allowed new implementors to be created without much interference in the logic of already created classes.

Can I accept a nop?

This question is quite often asked as the first one, which many times lead to big interfaces. Before you start noping your methods ask yourself: can we accept dummy implementation of encryption? Does it make any sense to lie to our clients that we encrypt the data in any meaningful way? In my humble opinion, encryption is used when we really care about secrecy and removing it would make our products incomplete and not market-ready.

The idea is simple of noping: instead of forcing everyone to implement every method, allow them to provide dummy logic/implementation. In case of

void doStuff() = 0;

not implementing it shouldn’t cause much problems. But what if our doStuff looks like this:

IBar* doStuff(IBaz& output2) = 0;

? Such methods require a global commitment where we establish a law that you should return nullptr if you don’t implement the method, and you shouldn’t modify the output parameters in any way.

While this solution works, it has at least two problems. First, it requires handling nops result in every client using the interface (which is quite ugly & copy-paste-ish). Second, we deal with much more fundamental issue: why do we use interface when we don’t require implementation? I mean, the essence of interface is that we can depend on some invariants/requirements and build our code on them. In my understanding all the logic should be done in implementors. If we force clients to handle various nop scenarios then something feels wrong.

I don’t want to sound too harsh here, because I know some libraries use this approach to allow heavy modification of code logic via interfaces. This is a valid method to deal with soft-requirement-type (optional) methods, but think twice before going this road. It might lead to architecture where everything is so optional that the foundations on which you build your code are very unstable.

Nop part II: exceptions

Another way of dealing with nop methods in interface is the use of exceptions for not implemented methods.

class IStream {
public:
	virtual ~IStream() {}
	virtual void push(int val) { throw new Exception(); }
	virtual int fetch() { throw new Exception(); }
};

class OutputStream : public IStream {
public:
	virtual void push(int val) { /* ... */ }
	// fetch() will throw if called
};

This abomination is used where we run into an issue where we have both: wide interface with optional requirements AND non-nopable logic, which we either can’t nop due to language restrictions or because of logic reasons (if you fetch something you don’t want to get nulls/0s all the time, because it doesn’t have any sense).

If you ask me, that would be the last thing I would use in my code. It creates an unwritten rule that only a part of implemented interface can be used, and those exceptions lurk around to show their head right during a presentation for your client. What’s more, the implementation hiding provided by the interface is thrown out the window, as you have have to know what class you received. I mean, isn’t dynamic_cast just about the same?

Is the interface even a good idea?

As you already see, the interfaces aren’t as nice as the OO books tend to describe them. They definitely provide a clean abstraction mechanism that defines the architecture … at a cost. Static nature of interfaces force us to go great lengths when allowing broader range of methods to be used, which leads to a problem of noping and partial implementations.

What if I do have some defined set of functions that I would like to call and I don’t require them implemented in any way (fully optional)? If you have one way channel then we get to land of … events and messages!

Think of it: you may have 1000s of events defined and 800 of them never used by anyone. Should you care? Not one bit (just skip the question why the heck did you implement 800 events which are not used). You add 15 more messages. Do your clients care? Nope, unless there are really interested in the new batch. You just got yourself a very flexible interface implementation … at a cost :). Now you live in a land where nothing is certain and debugging might be hell on earth where all the events are running around, calling different events in random order, etc.

Hope this helps some of you look at the interfaces from different perspectives.

Advertisements