In software development, it is very important to continually learn and experiment with new techniques for building great software. Our job is to learn and improve the quality of code that we build over time. It is through this progression of experimentation, failing, succeeding, observing, reading, listening, and teaching that we as developers improve. In this post, I am going to discuss one way in which we as developers have learned something great and have taken it to the point of failure. This will allow us to learn, resolve, create new techniques, and succeed in the future.
What is Dependency Inversion?
One of the most important SOLID principles for testing our applications is the Dependency Inversion principle. I am very glad to see many more codebases using this technique as time goes on, however, I am seeing nearly as many others getting caught up in an easy mistake of using too much injection and putting interfaces on everything regardless of necessity. Lets start by defining dependency inversion so we’re all on the same page.
In object-oriented programming, the dependency inversion principle refers to a specific form of decoupling where conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are inverted (i.e. reversed) for the purpose of rendering high-level modules independent of the low-level module implementation details. The principle states:
- A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
- B. Abstractions should not depend upon details. Details should depend upon abstractions.
Reading directly from this, we can see that it says we should be depending on abstractions and in doing so we take a much lighter dependency. A dependency on an abstraction is not a big deal. A dependency on a concrete class is a big deal.
Is what I just said always true? No, it depends on the abstraction and the concrete class. For example, I make an exception for simple classes provided through the framework or language my code is primarily based on. For instance, I have no problem saying that my method depends on being given a DateTime object since there is no real gain by abstracting that object since that is a part of the framework and I will always be injecting in an object of that type.
When are interfaces and injection being overused?
One example of over-architected injection that I see a lot comes from something that I’ve taught many times when explaining SOLID. It’s the classic example of a dependency that people don’t realize they have.
I assume most of my readers know that this is a dependency taken directly on the system clock. It’s important to inject this dependency when your code behaves differently based on the value returned from that static method. When we extract that dependency, people have been taught (by many people including me) that a good way of testing this is to have some kind of IClock interface with an implementation of SystemClock accessing the computers clock by calling that static method we were just discussing.
Now when we write our method, we just give it a parameter of IClock. When we run our code in production, it’s taking in the SystemClock object, and in our tests we’re using a mock object. Great! We’ve reversed the dependency! Is that really what we wanted though? It might have been, but in some cases we could have just passed in the DateTime instance object returned from that method. This would have reduced complexity in our code.
Because we used this interface instead of just depending on simple concrete classes, we’ve actually made our code more difficult to use and maintain. Isn’t that the whole reason we were following SOLID in the first place? Yes, yes it is. This is of course a simple example, but you can find larger examples of where you could test your code and build in a nice structure without having to create pointless interfaces. Heck even in this example, you might have a good reason to need that Interface, but I am sure that you could get away with using it less. Every time you add that layer, you add complexity. Sometimes adding it is OK, because it gives you a much needed seam, but I doubt you need it as often as you think.
Please make sure that you need the interfaces and layers of abstraction before creating them. I’ve come across many codebases that become as complex or more simply by trying to do things “the right way”.
The road to software development hell is paved with good intentions.
Don’t like the way I phrased something? Think I overgeneralized? Did I make a mistake?
Call me out on it!
I appreciate the feedback.