Why interfaces?
published: Tue, 8-Mar-2005 | updated: Mon, 16-May-2005
A friend who uses Delphi had this to say about a recent article of mine:
I have to admit to still not being real comfortable with interfaces. I've played around with them some, but I still haven’t had an “Aha!” moment with them where it has become clear to me why they are better than plain old abstract classes.
I dashed off a reply but I thought it would be a good topic for a post. Some of what I say here applies to any language that supports interfaces (like C#, B.NET, Java, Delphi for .NET) and some just applies to 32-bit Delphi (D32). I'll certainly state if what I say is D32-centric or not.
Interfaces are an important OOP design paradigm. In traditional OOP, we are concerned with inheritance, encapsulation and polymorphism. All admirable qualities, yet we are forced to design inheritance hierarchies that are inherently coupled, at least by the ancestor- descendant relationship. This can cause issues like certain methods being introduced at an early level in the inheritance tree that only apply to certain, but not all, descendants.
An example of this for Delphi programmers is the VCL's TList (an array-like container class much like ArrayList in .NET). The original TList stored pointers, and pointers only. It made no attempt to free memory associated with the pointers it contained when it was itself freed, mainly because it could not tell the size of the memory block associated with each pointer (or even if there should have been extra processing involved in freeing each pointer.
At some point in Delphi's life (Delphi 5, I think, but I no longer have it installed) code was added to call a virtual method (called Notify) when items in the list went away, so that you could do some clean-up work. The reason? A TObjectList descendant whose items were objects had been added to the VCL. Other TList descendants don't use this functionality at all (TClassList being an example). So we have coupling between an ancestor and a descendant, and the ancestor just contains code to satisfy one of its descendants.
Interfaces relax this traditional OOP design paradigm by merely defining behavior in the form of methods and properties that classes should define when implementing the interface. An instance of an implementing class when cast as an interface will only show the interface's methods. That's all. Using interface pointers (as Delphi calls them) or interface instances (as most everyone else calls them) then becomes very simple: there's no extra baggage brought along by the implementing class to worry about; all we see is the interface. This enforces our code to respect the interface and not go spelunking into the implementing object's code.
So there is a little bit of inheritance there, a bit of encapsulation, and a bit of polymorphism, but everything is so much more relaxed.
The magic thing here is with testing. If you write a method that accepts an instance of an interface, you have no idea inside the method which class is implementing the interface (yes, usually you can find it out but you shouldn't). So you can pass in to the method a test object (usually known as a mock object) that implements the interface, rather than the one you would usually use. This gives you all sorts of important benefits, the main one being that test objects are very light-weight (and don't do much apart from supply canned responses) compared with the heavy-duty real objects (like database connections, files over a network, HTTP sockets, or whatever).
If the run-time library for your language has a rich set of small highly-focused interfaces (such as the .NET Framework or the JDK), you can take advantage of the supporting classes that "know" about these interfaces. For example, in ASP.NET the data grid can bind to anything that implements IList, anything from a simple array to a full-blown dataset. Unfortunately the Delphi VCL is old-fashioned in this respect: no effort has been made to refactor parts of it into a well- designed set of interfaces.
You can also write a class to implement several interfaces. Neato. Until you need it you don't realize how important it is. It helps if your interfaces are small and highly focused, of course, which goes back to the support in your run-time library. Note that this is not multiple inheritance as in C++, but more of a poor man's equivalent. However for languages that don't provide multiple inheritance, the implementation of several interfaces is a major benefit that abstract classes cannot provide.
An example: in .NET the Int32 class implements IComparable (so not only can you compare two integers the long way round, but you can insert integers to a container that stores IComparables and that knows how to sort them), but it also implements IConvertible (so that it uses a standard way to convert itself to a string). Again, though, this relies on the run-time library having a rich set of interfaces.
(D32 only) Interface pointers are reference-counted. Every time you make a copy of the interface pointer the reference count is incremented, every time it goes out of scope the count is decremented. What this means is that if you are writing Win32 code you don't have to worry about freeing the objects behind the interface pointer: the run-time library will do it for you when the reference count reaches zero. This is quite neat for a language that relies on the developer doing his own memory management.
For me, the most important thing is the granularity you can achieve in defining behavior. It frees you so much when you design a system to be able to define behavior as a set of interfaces and then to test your code with mock objects.
(Note to followers of Ruby. Ruby doesn't have interfaces but instead has what are called mix-ins. A mix-in is a way of injecting new methods into a class, so what you get is essentially "interfaces with implementation". It works because Ruby is interpreted rather than compiled. I'll be talking more about this at a later date as I continue in my quest to learn Ruby.)