A coworker recently asked if we should always abstract every object into an interface in order to fulfill the Dependency Inversion Principle (DIP). The question stunned me at first, honestly. I knew in my head that this was a bad idea – abstracting into interfaces for the sake of abstraction leads down the path of needless complexity. However, I wasn’t able to clearly answer his question with specific examples of when you would not want to do this, at the time. I’ve been thinking about this for a few days now and I think I have a good, albeit very long winded, answer.
Before the question is answered, though, we need to step back and look at what DIP is all about. I’ve previously shown how to implement DIP and talked about why it’s beneficial, so I won’t be repeating that here. Rather, I want to talk about the language that describes DIP and what it really means.
A. High level modules should not depend upon low level modules. Both should depend upon abstractions.
B. Abstractions should not depend upon details. Details should depend upon abstractions.
The word ‘abstraction’ is used three times in the definition for DIP. So, in order to understand DIP, we have to first understand some of the basics of Abstraction.
Some Background On Abstraction
From Wikipedia (emphasis mine):
“In computer science, abstraction is a mechanism and practice to reduce and factor out details so that one can focus on a few concepts at a time”
I can’t say it any better than this.
Abstraction can very directly lead to a system that is more understandable by helping us ignore the detail and implementation specifics, allowing us to focus on something at a higher level. This reduction in cognitive load can benefit someone that is reading the code by not forcing them to know the detail immediately. Additionally, abstraction is a form of encapsulation or information hiding, which again helps us to reduce cognitive load and produce better systems. From Wikipedia’s entry on Information Hiding:
“In computer science, the principle of information hiding is the hiding of design decisions in a computer program that are most likely to change, thus protecting other parts of the program from change if the design decision is changed.”
At the heart of abstraction and information hiding, we find the ability to change the system. The ability to change is an absolute requirement in software development and produces good design that is easier to work with, modify, and put back together as needed. The inability to change is directly called “bad design” by Robert Martin, in the DIP article.
Applying Abstraction And Encapsulation To DIP
So how does our knowledge of abstraction and information hiding play into DIP?
First and foremost, DIP never states that we should depend on explicit interfaces. Yes, in C# we have an explicit Interface as a form of abstraction. It is a separation of the implementation detail from the publicly available methods, properties, etc, of a class. Some languages, such as C++, don’t have an explicit construct for interfaces, though. From Robert Martin’s original DIP article, again:
“In C++ however, there is no separation between interface and implementation. Rather, in C++, the separation is between the definition of the class and the definition of its member functions.”
A String As An Abstraction
Abstraction doesn’t always mean explicit interface constructs, as evidenced in C++. Nor does it always mean an abstract base class, which we also have available in .NET. In fact, languages such as Ruby don’t really need either of these constructs. The duck-type nature of Ruby allows an implementation to be replaced at any point, without any special constructs. In .NET, though, there are a number of abstraction forms that we can rely on, explicitly. We have the obvious interfaces and base classes (abstract or not) – but we also have constructs like delegates and lambda expressions, and even the simple types that are built into the base class library.
Let’s look at a simple string to illustrate abstraction. As I said in my SOLID presentation at ADNUG, we can invert our dependency on database connection information. Rather than putting a connection string directly into our code that calls the database, we can use the string as our dependency and our abstraction. All we need to do is follow the basic DIP principle and provide the string as a parameter to the class that calls the database. We certainly don’t need (or want, for that matter) to introduce a new interface or base class at this point. Our abstraction is simple enough to use a common type found in the .NET framework.
Other Forms Of Abstraction
Even if we are talking about an object, who says that the interface we are depending on has to be an explicit interface construct or base class? When I write a Domain Service that uses an Entity from my Domain, I don’t create an explicit interface for that Entity. Rather, I use the Entity’s inherent interface – it’s public methods, properties, etc.
I also use delegates on a regular basis. By specifying my abstraction as a delegate, I can further decouple the depending object from the dependant code that it needs to call. I’d be willing to bet you have used delegates as abstractions as well. Have you ever created an event handler for something like a button click? There’s a delegate’s abstraction at work.
The point is, there is not always a need to introduce an explicit interface or base class when inverting our dependencies. We still need to apply dependency inversion and provide our implementation as a constructor parameter (or setter, though I don’t like setter injection). But, that dependency doesn’t have to be anything more than the interface inherent to the object, or a simple type found in .NET.
You do have to be careful when making the call to not use an explicit abstraction with DIP, though. You can quickly turn your system into a ball of mud if you rely on a concrete class that is not intention revealing or well encapsulated to begin with. At the same time, too many abstractions can lead to needless complexity and make it very difficult to see the big picture of a system. Too few abstractions, though, will certainly lead to a rigid, immobile design that is hard to change. All of these problems are equally vicious – and I’ve been bitten by all of them in recent months. It takes good judgement calls to determine when you do and do not need an explicit abstraction for a dependency. Unfortunately, good judgement comes from experience and experience comes from bad judgement. Don’t be afraid to make bad decisions – make a decision, just be sure you can reverse that decision as easily as possible.