I like C# interfaces, as well as interface-based programming, proxies, stubs, and all the other interesting ways they can be used. However, as with many trends and fads in our industry, I think the use of interfaces has gotten a little out of hand. Today, interfaces are used:
- To implement aggregation instead of using multiple (or single) inheritance.
- For unit test mocks.
- For service definitions.
- To decouple every input or output of a class/function/service.
In fact, you could probably just boil it down to #4: decoupling is king! these days. Again, before you flip the bozo-bit on me, let me assert that I am a huge fan of decoupling. It is a key tool in abstraction and helps create modular, flexible designs. But as with anything it comes with a cost.
Consider the following C# scenario.
Class Foo uses an input class Bar in one of its methods. There are two definitions (both classes) and one relationship between them: just for argument’s sake, let’s consider that a complexity of 3.
To decouple these classes, you insert an interface definition IBar. Now we have three definitions, and two relationships; Foo uses IBar, and Bar implements IBar. This complexity is 5.
(Certain scenarios might also require the introduction of two new compilation projects at this point as well, but since that is highly variable we’ll just gloss over it for the moment.)
Every use of an interface introduces at least 2 new elements to every design; two new things that must be incorporated into every developers mental picture of the system; a new level of indirection that must be managed in debugging and diagnostics; new files in source control; new names in the namespaces, cluttering up Intellisense.
In a small design this may not be a significant cognitive burden and may have no impact on productivity at all, but in a large system of hundreds of core classes, decoupling can be a significant contributor to overall complexity.
This is not to condemn decoupling nor to say it isn’t a vital aspect of good design. But it is important to remember to consider the cost as well. For instance, when the sole purpose for decoupling is to support easier unit testing, is the added complexity justified? A knee-jerk reaction based on the sheer criticality of good unit tests would assert “of course!”. But decoupling to support mocks is not the only way to skin the cat. It might be better in the long-run to simply enhance your unit testing harness to operate in a real integrated data environment so that more of your system is tested with each test run. (Discussed in more detail here.)
Not all languages have first-class interface constructs of course, and they are not treated the same in all cases. C# actually has an especially problematic implementation of interfaces, so I think it is especially important to carefully consider the added complexity of introducing interfaces.
Everything has advantages, everything has disadvantages; the essence of turning “programming” into “engineering” is to always be aware of both and choose wisely based on that awareness.