When we looked at the Open / Closed principle, we saw that certain practices produced fragile code.
- Static Methods
- Non-virtual methods
- Creating objects
- Too-specific variable declarations
- Hard-coded values
- Using the wrong object (the Law of Demeter)
Of these, the first three are all things you can’t override. If you can’t override things, they’re inflexible and, inevitably, will cause problems. In the cases we’ve seen, the Law of Demeter problem could be solved just by using the right object in the first place. Equally, a non-virtual method is easy to fix, you just make it virtual. You might be thinking “but that line of reasoning would make every method virtual” and you’d be right. Private methods are pretty much the only case in which you don’t need this: because you can’t change a private method anyway.
Making Dependencies Explicit
Just for a second, let’s go back to the example of the code that printed some lines to the console. For the code to run, it needed two things: the lines themselves, and the console object. If we’d had some formatting rules, we could have obtained them from some instance variables. If we were writing to a file, we’d have need the StreamWriter object we created. These are all examples of direct dependencies:
- Static methods (and by extension, singletons)
- Objects we create
- Method Parameters
- Instance variables
Now, to go all Animal Farm on you for a second, two of these are good and two of these are bad. Static methods and objects we create can’t be changed, so they’re bad. Method parameters are easy to change: just change what you pass into the function. Instance variables are typically set either by the constructor, a property set or another method call. In each case, it comes down to a parameter you can change, so it’s good. Obviously, static methods and object creation has to happen somewhere, but there’s ways around that. You can wrap static methods with an object. You can then pass the wrapping object as a parameter. You can do the same thing with object creation. Then, of course, you’re using an abstract factory. You’ll have noticed that the discussion of SOLID principles is heading towards the same territory as the discussion of GoF design patterns. This is no accident, but I’ll cover that in more detail later on.
By doing this, we see something really interesting. All of a sudden, everything we depend on in an object is being passed in to the object. If we return to my naive interpretation of OOD, it’s completely the other way around. There, if a an object/actor needed another actor, he just got it, created it, whatever you like. Now, my object is always being told what to do. The actor is finally actually reading his lines.
It gets even better: hard coded values are dependencies as well. Again, the same solution presents itself: pass it in. Most people get the value of this when they start building tests for it, but it’s not about testing, it’s about flexibility. You can filter and buffer function calls by introducing decorators, you can change your real time data feed to a batch CSV import without altering any code.
We’ve already touched on how making variable declarations too specific can cause problems. In general terms, you want to declare variables with the most general type you can. In the file reading example, the correct parameter declaration for the input was IEnumerable<string>, rather than string array or “ILineProvider”. Abstraction is at the heart of the SOLID principles. If you declare something as IEnumerable<string>, more people can use your code easily than string array or “ILineProvider”. Another way of thinking about it is that you’ve made as few decisions as you can and still get work done. This directly analogous to the Agile principle of the Last Responsible Moment.
There are challenges associated with abstract code. The first is to get your own head round it. If you’re a mathematician, this is a lot easier: you’ve got years of training in it. The second is team acceptance. Because abstract code is harder to immediately grasp, it’s often derided as “unreadable”. In fact, well written abstract code can actually be significantly more readable than concrete code. If you think about sorting, would you rather have to implement a sorting algorithm every time you needed to sort something, or use a well-defined abstract algorithm and simply tell it how you wanted the comparison to be done? If you think about LINQ’s OrderBy method, there’s actually two layers of abstraction:
- It takes the fields you specify and turns them into a Comparison delegate.
- It applies a sort algorithm to the list using the Comparison delegate.
Depend on Abstractions, not Concretions
I’ve split dependency inversion into two parts: the injection of dependencies and the abstraction of dependencies. Although both are of use separately, it’s when we bring it together that it really starts to pay off. Using dependency injection without abstraction gives you some flexibility (e.g. the ability to read from a different file) but often not as much as you’d get from adding abstraction (e.g. the ability to read from a URL as well). Abstraction without dependency injection is, in many ways, worse, because you often get the illusion of flexibility without the benefits. You’re still performing wiring by soldering the lamp into the power socket. The fact that the power socket could power a toaster isn’t much use: the lamp can’t be pulled out without damage.
Understanding the power of abstraction and how to use it is key to all serious development. There are many things that don’t benefit: administration scripting (when you’ve got no re-use cases), project planning, end-user testing, work prioritisation… but if you want to be a better coder, you’ve got to learn abstraction.