Earlier I asked whether people thought the issue of passing in too many dependencies into a class was an actual problem. This was in response to another post I did about test smells. A few people had asked me how the problem of a controller having way too many dependencies should be solved. My response to them was to apply Single Responsibility Principle, which is pretty much what others also said.
The problem
Here is the problem:
In this case it is a controller, but it can well be any other type of class.
Dependency Injection does not solve tight coupling
Why is this a problem? After all, we are applying Dependency Injection.
We are, but we are not decoupling classes. All we are doing is allowing a specific implementation to be swapped out easily. Whether we’re doing it because we want to change behaviors in tests or at runtime, all we’re doing is avoiding depending on a specific implementation.
What we are not doing is automagically making our code clean and sustainable.
The more dependencies our class takes in, the higher its efferent coupling to other classes, meaning that it knows a little bit too much. If it knows too much, it is most likely because it is also doing too much. The class is no longer focused on one thing, and as a consequence we are creating somewhat brittle software that is too tightly coupled, even if we are passing in our dependencies, even if we are not using new anymore.
How did we get here?
We all agree that DI is good and so is Composition over Inheritance. And this causes us to end up with classes that act as coordinators or require information or perform actions on these different classes. Other times we end up here because of:
Framework impositions
Often, frameworks impose restrictions that take us down the wrong path. Such is the case of ASP.NET MVC when using the default routing conventions. We mostly encapsulate functionality under a specific controller because the routing suites us. In other words, from a user’s perspective it makes sense to have many things under the /checkout/{action} path, even if that leads us to bloated controllers that take many dependencies which have little to do with each other.
Badly named classes
XYZService or XYZManager named classes are yet another example of ending up with too many dependencies. When we start to encapsulate everything under a service or manager, we find that despite some of the dependencies only being used by one or two methods, still have to be passed in.IoC Containers
As developers we no longer worry about wiring up all the different required dependencies as Containers now do this for us. This allows us to inject and inject and inject.Helping solve the problem
First and foremost, and despite it being beaten to death, we need to apply Single Responsibility Principle. I've written in the past techniques that have often helped me accomplish this. Taking this into account, there's also a series of Do's and Don'ts that can help:What you can do
- Work around framework limitations. For instance, in the case of ASP.NET MVC, you can override the routing behavior to not tie you down to using one class just because you need to keep the same URL. This can be done in various ways. Google Controllerless Actions for some examples (just a starting point).
- Create small focused classes. Why create a CustomerServices that takes in five dependencies as opposed to creating MakeCustomerPreferredCommand and InvalidateCustomerCommand class that takes one dependency and has a single method to do what is required?
- Extract infrastructure. Don't pass in dependencies that have to do with infrastructure, specially if these are repetitive. Use techniques such as AOP to apply these at the level where they belong.
- Think about Patterns. Often patterns can help solve the problem of too many dependencies. For instance, many times, what's deemed as a large collaborative class can instead be turned into a series of classes using the Chain of Responsibility pattern.
- Events. Event driven architecture is a great way to provide higher decoupling and independence between objects.
- CQRS. Well I just had to mention this one because it's fashionable. In all seriousness though, this also allows for lower of dependencies since it separates responsibilities into smaller commands and separate queries, which again is our goal.
What you shouldn't do
- You shouldn't switch to using Service Locator as opposed to Dependency Injection. All you're doing is hiding the problem. Dependency Injection is actually beneficial in that it is showing us that we are breaking SRP and have a high coupling.
- You shouldn't stop using an IoC Container. There are other ways to prevent the Inject Happy Developer. Testing your code is one of them. As you write tests, you'll suffer the pain of having to set things up over and over again (and refrain from encapsulating that).