Dependency Relationships and Injection Modes: Part 2

Last time, I wrote about primal dependencies and the constructor injection mode. I also mentioned that in Part 2 I would cover "the other mode". In contemplating this other mode, however, I came to the conclusion that there are in fact two other modes. I'll cover both of them here in Part 2, but first we'll tackle mutator injection and optional dependencies.

Mutator Injection

Let's get the terminology out of the way first. By mutator, I mean a property or method on a class or object that is used to change some bit of state in that class or object. By optional dependency, I simply mean one which is not required in order for the object to accomplish its primary purpose.

In most code you'll read, mutator injection is ubiquitous. However, I would argue that most are misused because the usage, the need, or both, do not fit the above description of an optional dependency. So what are people doing with dependency mutators that they shouldn't be? And what should they be doing instead? Most people use mutation when they are presented with an apparent hurdle to using another, preferable injection mode. Often this comes down to an inconvenient design choice external to the class that creates an environment where it is hard to do the right thing. The appropriate response is of course to fix the context, whenever possible.

Why You Should Prefer Alternatives

Often, a dependency mutator is used to handle post-construction initialization of a primal dependency, where the appropriate dependency instantce is not known or available when the consumer is constructed. I would argue that this is very rarely an inevitable state of affairs, and is commonly due to a dependency cycle between two layers of the architecture. (I.e. an object in one layer depends on an object in the layer below it, and another object in that layer depends on an object in the first layer.)

Intra-layer dependency cycles are almost always a bad idea, not just because it makes it tough to avoid mutator injection. Maintaining a single direction of intra-layer knowledge also helps to prevent unnecessary coupling, and it aids in keeping each layer cohesive. But an extra benefit of fixing this situation is that it will typically allow for the primal dependency to be created or retrieved when it is needed for the constructor injection, rather than by the consumer or a manager object.

Why does this matter so much? Most simply, because mutator injection is a state change. And state changes are big potential points of failure. It's also well-established that it's easier to reason about immutable data structures. I've found this to be true in my own experience as well. It's easier to compose behavior out of pieces that exhibit immutability because it simplifies the interaction graph. Furthermore, when things go wrong fewer state changes means a shorter list of possible places where the bad state may have been triggered. So when designing my classes, I avoid mutators wherever I can, and that means preferring constructor injection and site injection over mutator injection whenever possible.

The other way that mutator injection is mis-used is in a situation I briefly mentioned in Part 1: when only a subset of the operations on a class actually make use of the dependency. Mutators are often seen as convenient in this situation because they cut down long constructor argument lists, which are seen as messy or complex. The fact that the object has at least a limited use without the dependency makes it possible to get away with setting it later.

In Part 1 I said that the situation itself is often due to a violation of the Single Responsibility Principle. It would be facile to cite this reason for all instances of mutator injection. Sometimes it's true and sometimes not. When it really isn't true, there are a couple of other options available before you have to resort to mutator injection.

The first option is again to drop back to constructor injection, but to do so with a sort of lazy proxy in its place. The lazy proxy exists for the sole purpose of deferring the resolution of the actual service dependency until it's needed. This is its Single Responsibility. It can then either provide an instance on demand, or it can be more transparent and just implement the same interface and delegate to the underlying object.

In a static language like C#, this isn't always possible either. Sometimes there's no interface, and some of the members are not virtual, so they can't be overridden in a derived class. If you control the code of this object, I highly recommend refactoring to an interface to resolve this problem. But if you don't, it might be time to look at the third injection mode: site injection.

Temporal Dependencies and Site Injection

Site injection is a fancy name for a method argument. It describes the use of a service as an argument to the operation for which it will be used. Such a dependency, that is passed directly to an operation, and for which no reference is maintained after that operation completes, is called a "temporal dependency". The reference is fleeting, and not actually part of the state of the object that depends on it.

There are a couple consequences of this type of injection. First is that the calling object or objects become the primary dependents of the service. It may or may not be easier to manage the dependency there. Hopefully it is, but sometimes it isn't, especially if there are many different objects that may call on this operation.

The other consequence of site injection is that it can add noise to the interface/API. If there are multiple operations in the subset of the dependent object that make use of the service, the repeated passing of the service into the objects can feel like unnecessary overhead. Not performance-wise, but in terms of keeping the code clean and readable. Expression of intent is paramount here. If the extra arguments communicate the true relationship between these two pieces of your architecture, then it's not truly noise. Instead it's a signal to readers of what's going on, and who's doing what.

There are lots of places where site injection and temporal dependencies are very natural. So many that you probably use them without even thinking about it, and it's often the most appropriate thing to do.

Truly Optional Dependencies

That's probably enough about what isn't an optional dependency. So what is? I would argue very few things are. Usually an optional dependency crops up as something that can be null when you want to do nothing, or can have an instance provided when you want to do something. For example, one step of an algorithm template, or a logging or alert solution with a silent mode. Often, even these can be treated as primal dependencies from the consumer's perspective, with a bonus of avoiding null-check noise. Just provide a Null Object by default, and make a setter available to swap the instance out as necessary. If you can say with certainty that this is not appropriate or feasible, then you may have a truly optional dependency.

A Good Start

I believe these three dependency relationships, and the injection modes associated with them, cover most of the scenarios that you'll encounter. And I've found that the advice I've included for normalizing to these modes has helped improve the design of my code in a very real and tangible way. But I'm certain my coverage isn't complete. There are surely more dependency relationships that I haven't identified here, especially if you dig into the nuances. I'd love to hear about other types of dependency relationships you've encountered if you're willing to share them in the comments.