Avoiding Dependency Injection’s Constructor Injection Hell by Using the Custom or Default Pattern
Update: Year 2019 – Even though I wrote this, I no longer agree with some of this
Dependency Injection makes the claim that a concrete implementation of an interface is going to change. The problem is that theory rarely meets reality. In reality, there usually exists one single concrete implementation of most interfaces. Usually there exists one Test implementation. Other than those two concrete interfaces, a second production implementation of an interface almost never exists. When there are multiple concrete implementations of an interface, it is usually in a plugin situation and is handled differently, using plugin loading technology. So we can exclude plugin scenarios, whether UI or other, and focus on day-to-day dependency injection.
In some projects, 100% of the objects have a single production implementation and a single test implementation. Other projects, you may see 90%/10%, but again, rarely are their multiple.
In other projects, you will find a second production concrete implementation, but it is replaces the previous implementation, not running alongside it.
So why are we not coding to the common case?
Construction Injection
Constructor Injection assumes a few things:
- Your class should not even instantiate without something injected into the constructor.
- Your class should not work without something injected in.
- Your class should have zero dependencies, except of standard types and simple POCO model classes.
It sounds great in theory, but in practice, it usually leads to constructor injection hell. If you are wondering what Constructor Injection hell is, have a look at this previous post: Constructor Injection Hell.
Property Injection Issues
Property Injection or Method injection are also assuming a few statements are true.
- Your class can be instantiated without something injected into the constructor.
- Your methods should throw exceptions if a required property has not yet been injected.
- Your class should have zero dependencies, except of interfaces, standard types and simple POCO model classes.
Method Injection Issues
Method Injection or Method injection are also assuming a few statements are true.
- Your class can be instantiated without something injected into the constructor.
- Your methods should throw exceptions if a required property is not injected when the method is called.
- Your class should have zero dependencies, except of interfaces, standard types and simple POCO model classes.
Dependency Injection Problems
There are problems caused with Dependency Injection.
- Dependency Injection Hell
- Your code doesn’t work without an injected dependency
- You cannot test your code without an injected dependency.
Should You Stop Using Dependency Injection
So now that you’ve been educated on the truth that theory and reality never mix, and most of Dependency Injection is a waste of time, let me tell you: Keep doing it because you still will benefit from Dependency Injection.
Don’t stop doing it, but instead add to it a simple pattern. The Custom or Default pattern.
Custom or Default Pattern
Upate 2022: I was wrong when I wrote this. Instead, one should use DI to inject a default or a custom. I no longer agree. However, the reason it makes sense was during refactoring legacy code. I now think that new code should never run into this, but old code that is bad and needs to be refactored can benefit from this during a transition period, however, at the end of the transition period, such a pattern should go away.
This pattern is so simple that I cannot even find it mentioned in any list of software development patterns. It is so simple and obvious, that nobody talks about it. But let’s talk about it anyway.
Make everything have a default value/implementation. If a custom implementation is supplied use it, otherwise use the default.
It is a simple pattern where all the Dependency Injection rules are simplified into two:
Your class should instantiate without something injected into the constructor.Your class and method should work without something injected in.- I now agree that one should depend on an interface that is injected in
- The default implementation should be configured in your composition root
Your class should have zero dependencies, except of standard types and simple POCO model classes or default implementations.- Classes can depend on standard types included in dotnet, simple POCO models, and interfaces.
Notice the rules change. And these changes make your life so much easier.
What adding this pattern says is that it is OK to depend on a default implementation. Yes, please, support Dependency Injection, but don’t forget to provide a default implementation.
Constructor Injection with the Custom or Default Pattern
This pattern could be implemented with Constructor Injection.
public class SomeObjectWithDependencies { internal IDoSomething _Doer; public SomeObjectWithDependencies(IDoSomething doer) { _Doer = doer ?? new DefaultDoer(); <-- This is coupling (I no longer agree) } public void DoSomething() { _Doer.DoSomething(); } }
However, you didn’t solve Constructor Injection hell. You could solve that with a Service Locator. Here is the same pattern with a Service Locator. I’ll only do one Service Locator example, though it could be done a few ways.
public class SomeObjectWithDependencies { internal IDoSomething _Doer; public SomeObjectWithDependencies(IDoSomething <span class="hiddenGrammarError" pre="">doer) { doer</span> = doer ?? ServiceLocator.Instance.DefaultDoer; } public void DoSomething() { _Doer.DoSomething(); } }
Many developers will immediately react and say that both example result in coupled code. SomeObjectWithDependencies becomes coupled to DefaultDoer or ServiceLocator.
Well, in theory, this is coupling. Your code will never compile, the earth will stop moving, you will rue the day you made this coupling, and your children will be cursed for generations, blah, blah, blah. Again, theory and reality aren’t friends. The code always compiles. The coupling is very light. Remember, it is only coupled to a default. You have now implemented the Custom or Default pattern. With one line of code, you solved a serious problem: With regular Dependency Injection, your code doesn’t work without an injected dependency.
Guess what, you had coupling anyway. Usually you had to spin up some third library, a controller or starter of some sort, that hosted your dependency injection container, and then you had to couple the two objects of code together and now you have an entire third library just to keep two classes from being friends.
The above is still very loose coupling. It is only coupling a default. It still allows for a non-default concrete implementation to be injected.
Property Injection with the Custom or Default Pattern (Preferred)
This pattern could be implemented with Property Injection. However, this pattern suddenly becomes extremely awesome. It is no longer just a standard property. It becomes a Lazy Injectable Property.
A Lazy Injectable Property becomes key to solving most of your Constructor Injection hell.
Since it is very rare that a implementation other than the default is ever used, it is extremely rare that a Dependency Injection container is ever really needed to inject a dependency. You will find yourself doing Dependency Injection with a container.
This item
public class SomeObjectWithDependencies { public IDoSomething Doer { get { return _Doer ?? (_Doer = new IDoSomething()); } set { _Doer = value; } } private IDoSomething _Doer; public void DoSomething() { _Doer.DoSomething(); } }
Or if you have a lot of such dependencies, you can move them into a DefaultServiceLocator .
public class DefaultServiceLocator() : { public IDoSomething Doer { get { return _Doer ?? (_Doer = new ConcreteDoSomething()); } set { _Doer = value; } } private IDoSomething _Doer; // more lazy injectable properties here } public class SomeObjectWithDependencies(IDoSomething doer) { public IDefaultServiceLocator ServiceLocator { get { return _ServiceLocator ?? (_ServiceLocator = new DefaultServiceLocator()); } set { _ServiceLocator = value; } } private IDoSomething _ServiceLocator; public void DoSomething() { ServiceLocator.DefaultDoer.DoSomething(); } }