Dependency Relationships and Injection Modes: Part 1
This week I'm going to continue my trend of adding to the cacophony of people talking about IoC and DI. There's a lot of talking, and a lot of it is noise. Not inaccuracy, mind you. Just an awful lot of shallow coverage, one step beyond assumptions and first principles, with not a lot of real analysis. So let me bring a bit of deliberate consideration and analysis to the table, and hopefully improve the signal to noise ratio.
What's different this week from the recent past is that I am specifically going to talk about DI for once, rather than IoC in general. The particular topic I want to address is something that I've seen addressed a few times, but usually in passing reference in the context of some other argument. And most often it's being both taken for granted and dismissed out of hand, by the respective debaters.
In most languages there are essentially two injection modes available: via a constructor argument, or via a mutator such as a property setter or method. I've found that each option is most appropriate in different situations. A constructor argument dependency expresses a relationship that is very different than that expressed by a mutator.
Conveniently, there are also two broad types of dependencies that map fairly well to these two modes. The first is sometimes termed a "primal dependency". (I'd love to credit the inventor of this term, but all i can find in a Google search is Jimmy Bogard, Tim Barcz, and Steve Harman all using the term as if it already existed.) The second type of relationship is most often called an "optional dependency", which is a much less evocative term, but it's a descriptive one nonetheless.
The relationship that is being evoked by the term "primal dependency" is one of raw, fundamental need. A primal dependency is one that must be satisfied in order for the class to even begin to do its duty. When a primal dependency is unfilled, the dependent object is essentially incomplete and nonfunctional. It's in an invalid state and can't properly be used.... At least not if the SRP, or Single Responsibility Principle (PDF link), is being obeyed.
The SRP is one reason why I advocate using constructor injection for primal dependencies. Constructor injection makes it explicit and unavoidable that a given dependency is a requirement for proper functioning of the object. If a dependency could be provided via a mutator without affecting the post-construction usefulness of the object, then really you have one or both of two situations. Either it's not really a primal dependency, or your object is responsible for more than one thing and this dependency is only required for some of those things. Using constructor injection avoids the possibility of this signal being hidden behind the guise of post-construction initialization.
When people oppose constructor injection, it's often on the claim that it puts an undue burden on the object performing the instantiation. While it's true that a burden is being transferred, I'd argue that it's not undue. Construction and initialization of an object into a fully valid state should be considered to be two parts of the same operation, the same responsibility. Furthermore, this responsibility is separate from the primary purpose for the existence of the object itself, and so it doesn't belong to the object itself. This is what DI is meant to resolve. Yet, the logic must be taken a step further.
If construction and initialization is truly a multi-step process pulling from different domains, and/or which can't cleanly be handled in one place, then it's a responsibility that's complex enough to base on entire object around. In short, you should strongly consider setting up a factory or builder object to handle this task. The only extra burden this leaves is on the brain of the programmer who is unfamiliar with the pattern. But trust me: just like with any exercise, the burden gets lighter with repetition.
I have a confession to make: I had never actually used the builder pattern until I came to this revelation. I didn't really understand what the benefit was over a factory. And I'd only taken the most rudimentary advantage of factories, for implementing concrete implementations of abstract base classes. But now each is as clear, distinct, and obvious of purpose to me as a screwdriver and a hammer. You'll find they come in quite handy, and really do clean up the consuming code, once you acknowledge that construction and initialization should often be a proper object responsibility all on its own.
In Part 2 of this topic, I'll address optional dependencies and the mutator injection mode. Like today's topic, they're deceptively simple to describe. But there is just as much, if not more, nuance to be found in the manner and purpose of their implementation.