SOLID: Dependency Inversion Principle
The last letter in the SOLID acronym stands for The Dependency Inversion Principle, or The Dependency Injection Principle. It states, “Depend upon Abstractions, not upon Concretions”. That is, do not allow high-level, broad objects to depend upon low-level, specific objects, instead create layers of abstraction in mid-level objects to “invert” the dependency.
The more abstracted objects are, the less likelihood of breakage if one of those objects is altered, and it also means that more functionality can be added with ease. Let’s look at an example. Let’s say we have created an online Media Store, which reads audio files from its library and writes them as downloads when people buy them. Considering the basic requirements at face value, it might look something like diagram below:
However, this set up causes the high level object of MediaStore
to be very dependent on the low level specific objects of AudioFileReader
and AudioFileWriter
. The low level objects’ specificity makes them very concrete. This means that if we were to edit these low level file accessors, we risk altering the entire high level function. And what if later requirements considered adding movie files? That might mean we would have to duplicate a lot of read and write functionality in order to create alternative specific movie objects. To keep things clear and minimise fragility we should add a layer of abstraction, like so:
Here we have used a level of abstraction in Reader
and Writer
to “invert” the dependency. Now the media store depends on reader and writer classes that are non-specific, and that also means that the reader and writer classes need only concentrate on the specific details of reading and writing movie or audio files. We can see how Dependenry Inversion/Injection Principle is very closely related to the Single Responsibility Principle and the Open Closed Principle in this way.
Dependency Injection
Some people like to talk about D.I.P. as being about Dependency Inversion, and others about Dependency Injection. I like to think of it as Dependency Inversion being what the principle achieves, and Dependency Injection as how it acheives it. To me it must be both of these things, the ‘what’ and the ‘how’. Dependency Injection is more clearly seen within the code itself. Let’s think about software written to operate a copier. The copier must read from one source and write to another source. Let’s say it reads from the computer keyboard and writes to a printer. How might that look?
Can you see how concrete this example is? The Copier
class function depends heavily on the KeyboardReader
class for reading and the Printer
class for writing. This is not great. Let’s look at something preferable:
The above example is much more abstract, and therefore less fragile and more open to extension. The “injection” comes into play when we inject the copier’s dependencies on instantiation, at Copier.new(KeyboardReader.new, Printer.new)
. This is open to extension in the sense that the initialize
method would just just easily take other types of readers and writers such as a ScanReader
to an ImagePreviewWriter
.