WTF is Dependency Injection?
Absolute Beginner’s Guide to Dependency Inversion, Inversion of Control, and Dependency Injection in Go
In software, abstraction is the apex of any design. If you have written any code longer than a few hundred lines, then you would have realized how hard it was to read, make sense of, and maintain the interactions between those functions and objects. No matter how good and experienced you will become, digging into tons of tightly-knitted code will always be a challenge for you.
Dependency Inversion Principle is simply a guideline to write loosely-coupled code.
To understand what it’s all about, let’s take a look at what it solves:
The above Watcher struct is tightly-coupled with LogWriter since it includes the latter as its field, and its only method Notify call LogWriter’s method.
Now, why is this tight coupling a bad thing? Imagine you driving a car while maneuvering the steering wheel and at the same time having to think about the mechanism of the wheel controlling those on the ground instead of focusing on what’s natural — how steering relates to the change in motions you perceive from the environment around you and where you want to drive to. You wouldn’t survive a mile.
Any object shouldn’t be made to concern about what’s not its problem. Like us, when it can focus on only one thing, it can really do it well.
Similar to driving, tight coupling in code can lead to mishaps:
- What if LogWriter changes? Especially if its method Write sudden gone?
- What if Watcher should be able to write its notification to a log file too? Do we have to write another WriteToFile method? What if later it needs to also write to a database?
- There is no way to separately test Watcher without, in a way, test LogWriter too as a side effect.
Dependency Inversion simply suggests that a being should interact with an abstraction rather than the concrete implementation of another being (whether it’s a class, struct or function). To improve on the last code, let’s create an interface which LogWriter can implement.
Now Watcher no longer has to depend on LogWriter. It interacting with a StringWriter interface means that it doesn’t care (or know) about the internal implementation of the identity it calls. This is collectively known as Inversion of Control (an API delegates or inverts the control to the caller).
Dependency Injection (DI) is a practice of creating completely ignorant objects in the way that an object need not know anything at all about the creation of dependencies or try to create one inside itself or one of its method. In Go, DI is particularly useful for unit testing, although it is in a lot of scenarios. Consider this test code:
Both Watcher and Howler has no knowledge of what kind of io.Writer they will have to deal with, nor do they have to create one. It is solely the responsibility of the caller to create its own and pass it to them either during construction or via its method, depending on the design.
This makes it really trivial to test an object and swap out its dependencies. Note that in the above code, you can also isolate MockWriter object and test it.
Dependency Injection in use
DI can be used in general, especially when writing a set APIs whose internal the callers do not have any insight into or control over. To give the caller the control, a function should delegate as much as possible to the caller.
Can you imagine the first function, PrintIfMatchedHello, being used by any one else apart from the creator? It isn’t versatile and generic enough. The control lies inside the function, and it’s left to the mercy of the developer of this dictatorial APIs to change it to fit the uses of others.
With the last design, the caller becomes responsible of designing the matcher function’s logic, and hence gaining total control. All the API needs to know is that if the matcher function returns true, it will print the message.
I hope this will inspire many of you to go back and fix your code. Happy coding gophers.