Bad Coupling with Constructor Injection
20 August 2014
In previous posts, I started to explain the difference between Dependency Injection (DI) and a simple factory. To leverage more of the potential of DI, I gave an example of using constructor injection and contrasted that with setter injection. Like all things, too much of a good thing can have horrible consequences. Hopefully, this post can help you to identify when things are going wrong.
A few years back, Jeff Palermo posted about the "Constructor over-injection anti-pattern." While I appreciate the details of his post and his solution, I think it is very important to look at what he said in his update: "We don't fret over 2 constructor arguments. We fret about 5, 10, 20 [or more constructor arguments]." You absolutely can overdo it with the number of parameters that you pass in via constructor injection. But, to me, the number of dependencies that you inject is more of a cohesiveness problem than a performance problem.
To have good cohesion, classes should have a small number of instance variables, which includes dependencies injected into the constructor. Every method should manipulate/use all (or at least most) of those variables. The greater the percentage of instance variables used by a method, the more cohesive that method is to its class.
Although the second method in the code below is a bit useless in its current implementation (because it is identical to the first method), there is execellent cohesion in this class because all of the methods are using all of the class-level variables (i.e. "validator" and "repo").
The updated implementation below, is very similar to the first implmentation. But, in this one, there is an additional dependency that is only used by the UpdateOrder method. This is still a very good level of cohesion because all of the methods are using a large percentage of the class-level variables.
While the implementations above have good cohesion, the implementation below is a fairly obvious case of bad cohesion. The PlaceOrder method for domestic orders does not use the dependencies for international orders and vise versa. Notice how much more difficult it is to read this code. Everything is cluttered together. And, at first glance, it is difficult to see how pieces of the class relate to one another.
To solve this, consider breaking down your classes into smaller, more logically grouped classes. Low cohesion is an indicator that at least one other class is trying to get out of the larger class.
While cohesiveness can also be overdone, you do want to maintain a high level of it in your classes. The more dependencies that you inject into your class, the less likely it will be that your methods will be highly cohesive. By this I mean that despite the fact that methods are all in the same class and the developer thinks they are related, they are actually not related and therefore do not belong in the same class. If they did, they would depend on (mostly) the same things. In general, I think that 3 constructor injection variables is pushing it, and 4 or 5 is too many. But, I do not consider this a hard limit. Every situation is different.
Increased construction injection leads to the following:
- Possible performance issues (see the Palermo article)
- Increased test friction because of more dependencies to fake out
- Readability issues
- Maintainability issues
Do not think that you can mitigate this problem by using constructor injection to pass the dependency injection (DI) container into your class. This completely masks whether or not your class is cohesive and does not solve the problem. This is because there is no limit to the number of dependencies that can be pulled out of the DI container.
Other indications of poor class cohesiveness are generic names. Names such as "OrderManager" are a dead giveaway that there is likely low cohesiveness in a class. Names should be specific and descriptive of everything that a class does. If you cannot clearly describe all that a class does, then it is likely too large and lacks cohesion. An extreme case of low cohesion can lead to the "Blob" anti-pattern.
A better way to breakdown "Manager" classes is to follow common design patterns and name your classes based on the pattern it follows (e.g. OrderProcessingCommand and OrderConfirmationStratgy). By following design patterns and naming the classes accordingly, you are not only corralled into splitting functionality up appropriately, but you also organize your classes in such a way that you can easily identify the appropriate place to find what you are looking for.
Injecting too many dependencies into a class is an indication that your class is trying to do too many things. Although this opens the possibility of performance issues, it absolutely causes other problems such as increased test friction and reduced maintainability. Consider breaking down your classes into smaller, more maintainable classes and consider following common design patterns.
is licensed under a Creative Commons Attribution 4.0 International License