This is another post in the Silver Bullets series. This series presents a set of best practices, design and implementation principles to combat software complexity.
Interface-based programming takes information hiding and applies it to object-oriented programming. In this post I'll talk a lot about C++, but it applies to other statically typed programming languages like Java and C#. I will not discuss dynamically typed languages today.
In the Beginning there was COM...
It all started (for me) in the previous millennium. I was coding desktop applications on Windows and using the awesome COM (Component Object Model) technology. I liked the concept of component-based software. I was fascinated by the strong boundaries a COM object established and by the precision that went into declaring the interface in a separate language (MIDL). It worked really well, but it was all very mysterious to me. I didn't understand what's going on exactly and how it all mapped to the C++ concepts I was familiar with.When I joined a fresh startup armed with my beloved COM concepts I pushed very hard to convert a complex code base to a clean COM-based design. After a lot of persuasion on my side I managed to get most of the developers on my side and stopped development for a week to refactor the code base. Exhausted yet proud, I unveiled the shiny new component-based design to my fellow developers. Then, I tried to build it and all hell broke loose. My über-sophisticated refactoring didn't play very well with the Visual Studio build system and it was strange build errors galore. I got error messages from build steps I didn't even know existed. It took me and another developer two more weeks to get to a stable state with terrible hacks. Note, that it was totally my fault. The COM technology itself was flawless and the visual studio build system did exactly what it was supposed to do.
The Enlightenment
Then, I got my hands on the excellent book "Essential COM" by Don Box and started reading. Chapter 1 opened my eyes to a new world. Don Box explained in it the problem COM was trying to solve and basically developed COM from scratch using plain C++. No exotic CoCreateInstance() calls, no strange thread apartments. The basic idea is to physically separate interface from implementation. When you inherit from a concrete class in C++ or when you call a method through a pointer or reference to a concrete class your code must be linked against this concrete class and in cascading fashion to all the concrete classes it knows about (which often means the whole world).Check out the following code for a fictional yet very realistic example. Class A wants to call the foo() method of B. It gets a pointer to a concrete instance of B and as a result now depends on B's implementation, which depends on a factory, a database and some fancy service. Each one of them probably depends on many other classes.
class A { public: A(B* b) { b->foo(); } } class B { public: void foo() { DB& db = Factory::GetDB(); db.UpdateFooTable(); FancyService::ExecuteFancyAction(); } }Consider an alternative interface-based design. Instead of a pointer to a concrete instance of B, the A class now accepts a pointer to an interface IB. The B class implements IB, but A has no idea and doesn't have to be linked with B and its dependencies. At runtime the B class may be loaded from a dynamic/shared library and passed to A.
class A { public: A(IB* b) { b->foo(); } } struct IB { virtual void foo() == 0; }C++ doesn't have real interfaces. But, it's got pure virtual functions, which can be used for the same effect. I declare C++ interfaces as structs that contain only pure virtual functions. The reason I use a struct and not a class is that structs are public by default, so it saves me the trouble of writing "public:" and it also communicates the fact that everything here is public. I prefer not to specify a destructor, because object lifetime management usually doesn't belong on an interface. Remember the using object doesn't know anything about the implementation behaind the interface, so it shouldn't know when and how to destroy it. I will discuss object lifetime management in a future post.
What Else Is Cool About Interfaces?
Interfaces enable many other important best practices like dependency injection, mock-based testing, strategy pattern, interception and granular access to selective feastures.Dependency Injection
Dependency injection sounds scary and I actually prefer the term Third Party Binding. What it means it that a component can't instantiate its own dependencies or even look them up via some lookup service. This way the component is really decoupled from the implementation of its dependencies and once tested you know it will function as desgined regardless of the environment it operates in.
IB* b = new B(); A* a = new A(b);
Mock-Based Testing
Thoroughly testing a component using interface-based programming is made possible by replacing the concrete dependecies with mock objects that implement the same interfaces. This is powerful stuff. It allows you to simulate ANYTHING: Database failures, out of memory, network failures, network delays, other buggy components, accelerated time, etc. Let's say we want to test a class that fetches some data over the network through an interface called IDataFetcher. To make the example realistic the interface will be asynchronous and return a FutureData interface that has an IsReady() method that the caller can query.
struct IFutureData { virtual bool IsReady() = 0; virtual std::string GetError() = 0; virtual const std::string& GetData() = 0; }; struct IDataFetcher { virtual IFutureData& FetchData(std::string url) = 0; };
This is a real-world depedency that's really difficult to test with a concrete DataFetcher. But, with the following mock object it becomes a piece of cake.
struct MockDataFetcher : public IDataFetcher, public IFutureData { // IDataFetcher implementation IFutureData& FetchData(std::string url) { return this; } // IFutureData implementation bool IsReady() { return ready; } std::string GetError() { return error; } const std::string& { return data; } bool isReady; std::string error; std::string data; };
Suppose the object under test is an analyzer that aggregate data coming in from the data fetcher:
class DataAnalyzer { DataAnalyzer(IDataFetcher fetcher); AnalysisResult Analyze(); };
You want to test the Analyze() method that fetches data using the IDataFetcher and by providing the MockDataFetcher and setting its isReady, error and data members you can fully control what the DataAnalyzer will encounter when it tries to fetch data.
Strategy Pattern
The strategy pattern is a common design pattern that shows up in many large systems. The gist of it is that some algorithm embedded in a bigger process has severl alternative implementations and you want to dyamically select the algorithm. For example, suppose you write a data compression class and depending on the nature of the data to encode (test, audio, video) you want to select as proper compression algorithm. By having all compression algorithm implement a simple unified interface like
struct ICompressionAlgorithm { virtual void * compress(void * buff) == 0; }
You can have a compressor class that performs many common actions for any type of data, but differs only in the compression algorithm it is using:
class Compressor { public: Compressor(ICompressionAlgorithm& algorithm); void * Compress(std::string filename); private: ICompressionAlgorithm& _algorithm; }
Then you can implement many different algorithms for different types fo data and instantiate the single Compressor class with different concrete compression algorithms:
class TextCompressionAlgorithm : public ICompressionAlgorithm { ... } class VideoCompressionAlgorithm : public ICompressionAlgorithm { ... } class AudioCompressionAlgorithm : public ICompressionAlgorithm { ... }And finally instantiate dedicated compressor objects for different types of data:
// Instantiate a concrete compression algorithm VideoCompressionAlgorithm vca; // Instantiate a compressor passing the compression strategy Compressor videoCompressor(vca);
Remember to be dilligent about who's in charge of cleaning up all these objects. In this case I use a reference for the strategy which puts the burden on the codethat instantiates the Compressor class.
Interception
Interception is another interesting use case where interfaces shine. Interception allows you to intercept (duh!) method calls and do something before/after each call. Aspect-oriented programming is all about interception of course, but normally people associate it with special frameworks like PostSharp or even whole languages like AspectJ or at least some laguage features like decorators in Python.
But, this is a lot of magic and many people don't like magic. With interfaces you can implement explicit interception by wrapping the actual object that performs the work with a wrapper that exposes the same interface performs some actions before the call, calls the the main object and then performs some actions after the call.
The classic example is logging. Suppose you want to log every call to a certain class. You could of course add the logging code inside the class methods, but that will require the target class to be aware of the log (is it a file? print to the console? send something over the network?) or pass a logger object that knows all about that. But, even a logger object may be too complicted and require modifying the target class. Here interaction via an interface comes to the rescue:
But, this is a lot of magic and many people don't like magic. With interfaces you can implement explicit interception by wrapping the actual object that performs the work with a wrapper that exposes the same interface performs some actions before the call, calls the the main object and then performs some actions after the call.
The classic example is logging. Suppose you want to log every call to a certain class. You could of course add the logging code inside the class methods, but that will require the target class to be aware of the log (is it a file? print to the console? send something over the network?) or pass a logger object that knows all about that. But, even a logger object may be too complicted and require modifying the target class. Here interaction via an interface comes to the rescue:
struct IWorker { virtual void DoWork() = 0; }; class RealWorker : public IWorker { void DoWork() { // do some serious work here } }; class LoggingWorker : public IWorker { LoggingWorker(IWorker worker, ILogger& logger) { logger.Log("Before DoWork()"); worker.DoWork(); logger.Log("After DoWork()"); } };
By passing a LoggingWorker as an IWorker to code that used RealWorker before you get logging without modifying either the RealWorker or the using code. This works particularly well if you have a WorkerFactory that instantiates workers in a one stop shop, but that's outside the scope of this post.
More about COM and interfaces
COM goes much further than the C++ interfaces I discussed here. It is a binary component technology, usable in-process and out-of-process (and to some flaky degree even across machines using DCOM). It takes threading into account and it is cross-language inter-operable. It is very powerful, but can be very complicated to use and requires run-time support as well as some tooling to use effectively. All these capabilities were fine-tuned later and served as the foundations of the .NET framework. Interface-based programming is just one facet of COM, but it is the single most important aspect.COM also served as the inspiration to XPCOM a cross-platform COM implementation used as the foundation for Mozilla/FireFox and other successful applications like ActiveState Komdo.
Take Home Points:
1. COM is/was cool2. Interface-based programming is a mechanism for information hiding and loose coupling
3. Interfaces are the most important design feature for in-process component-based architecture