The 5 Principles of Dependency Injection
It’s easy to do and more than worth the effort
In my previous article, I discussed the what of doing dependency injection (DI) and how DI can help make your code more maintainable by taking advantage of abstractions and decoupling things.
This article is more practical, providing five ways you can go about doing DI — describing the how. Follow these five basic ideas, and you’ll be doing DI without much additional effort above how you code now.
Five Basic Principles to Follow
Code against abstractions, not implementations
Erich Gamma of the “Gang of Four” (the authors of the book “Design Patterns”) is credited with coining this phrase, and it’s a powerful and essential idea. If you teach new developers only one thing, it should be this aphorism.
Abstractions — usually interfaces but not always (see below) — are flexible. Interfaces (or abstract classes) can be implemented in many ways. Interfaces can be coded against before the implementation is even completed. If you code to an implementation, you’re creating a tightly coupled and inflexible system. Don’t lock yourself into a single implementation. Instead, use abstractions, and allow your code to be supple, reusable, and flexible.
Never create things that shouldn’t be created
Your classes should follow the single responsibility principle — the idea that a class should only do one thing.
If they do that, then they shouldn’t be creating things because that makes two things they’re doing. Instead, they should ask for the functionality they need and let something else create and provide that functionality.
Creatables vs. injectables
So what should be created? Well, there are two different kinds of objects that we should concern ourselves with: creatables and injectables.
Creatables are classes you should go ahead and create. They are run-time library or utility classes that are common and well-known.
Generally, classes in the run-time library should be considered creatables. Classes like this should not be injected but should be created by your classes. They often have short lifetimes, frequently living no longer than the span of a single method. If they’re required by the class as a whole, they can be created in the constructor. One should pass only other creatables to the constructor of a creatable.
Injectables, on the other hand, are classes we never want to create directly. They’re the types of classes we never want to hardcode a dependency to and that should always be passed via DI.
They normally will be asked for as dependencies in a constructor. Following the rule above, injectables should be referenced via interfaces and not direct references to an instance.
Injectables will most often be classes you write as part of your business logic. They should always be hidden behind an abstraction, usually an interface. Note, too, that injectables can ask for other injectables in their constructor.
Keep constructors simple
Constructors should be kept simple. The constructor of a class shouldn’t be doing any work — that is, they shouldn’t be doing anything other than checking for
null, creating creatables, and storing dependencies for later use. They shouldn’t include any coding logic. An
if clause in a class’s constructor that isn’t checking for
null is a cry for that class to be split into two classes. (There are ways to check for nil-value parameters that don't involve an
A complex constructor is a clear sign that your class is doing too much. Keep constructors short, simple, and free of any logic.
Don’t assume anything about the implementation
Interfaces are, of course, useless without an implementation. However, you, as a developer, should never make any assumptions about what that implementation is.
You should only code against the contract made by the interface. You may have written the implementation, but you shouldn’t code against the interface with that implementation in mind. Put another way, code against your interface as if a radically new and better implementation of that interface is right around the corner.
A well-designed interface will tell you what you need to do and how it’s to be used. The implementation of that interface should be immaterial to your usage of the interface.
Don’t assume an interface is an abstraction
Interfaces are nice, and I certainly sing their praises all the time. However, it’s important to realize not every interface is an abstraction.
For instance, if your interface is an exact representation of the public portion of your class, you really aren’t abstracting anything, right? (Such interfaces are called header interfaces because they resemble C++ header files). Interfaces extracted from classes can easily be tightly coupled to that class alone, making the interface useless as an abstraction.
Finally, abstractions can be leaky — that is, they can reveal specific implementation details about their implementation. Leaky abstractions are also normally tied to a specific implementation. (You can read more about this notion in an excellent blog post by Mark Seemann.)
The use of proper DI can make a huge difference in your code. Doing it properly isn’t terribly difficult, but it does require some forethought, planning, and design.
However, that minimal work will pay huge dividends when it comes time to maintain your code. Fixing loosely coupled code is a maintainer’s dream, and you should endeavor to write such code.