Let's address the fundamentals of Dependency Injection with a brief review of the dependency injection principle
When following this principle, the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are inverted (i.e. reversed), thus rendering high-level modules independent of the low-level module implementation details. The principle states
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
It appears that the first two approaches you mentioned may not be fully following DI principles because the higher order objects appear to need to know what type of Context
they should have.
And before I continue, I should admit a bias that will potentially offend fanatical DI proponents:
DI can get over-emphasized and zealous adherence to the principles can get in the way of writing maintainable code that's appropriate for the environment that it operates within. And that's not necessarily a dig against DI, but rather against any technique that ends up being blindly applied. I'm not saying that you are blindly applying DI, but there are many who fall into that category.
Spoiler used to protect those who are overly sensitive to such claims
So before calling methods 1 & 2 "bad", we need to ask if they are meeting your current needs. Those approaches involve some measure of boilerplate code that's copied around. This can be okay if you don't find yourself needing to change that template on a frequent basis.
Likewise, if the objects can use one and only one type of Context, then you may not need to worry about DI in this case as there isn't necessarily something else you'd inject into the object. "But what about testing?!" is the usual retort to that pragmatic claim, which is answered with an equally pragmatic answer of "Can those objects use / do they need a different Context for testing?" If yes, then use DI. If not, then ... carry on.
The 3rd option you propose gets you further away from the boilerplate code and gets you closer to a DI approach. Your hesitation appears to be:
but I'm not sure how to ensure that everything is using the correct child injector
I think the solution to that challenge is to wrap your Context creation within a Factory and absolve the higher order objects from knowing what they need to call.
So if I understood your code sample correctly, instead of this:
Bar bar = fooChildOne.getInstance(Bar.class);
you would have something like:
Bar bar = new Bar(ContextFactory.getInstance(Bar.class));
In the suggestion, we're passing the Context returned from the ContextFactory
into Bar
's constructor. It may be 1 trivial to simplify that example with:
public class Bar(){
Bar(){
return Bar(ContextFactory.getInstance(Bar.class));
}
}
or:
public class Bar(){
Bar(){
this.Context = ContextFactory.getInstance(Bar.class);
}
}
1 My Java is rusty, so I don't know if you can chain constructors like that first example.
And you could potentially tighten things up a bit by using reflection within the ContextFactory
to determine the correct Context for Bar
or optionally pass in additional parameters to the ContextFactory
if Bar
can handle multiple types of Context in regular operation.
TL;DR
Use the third option you proposed and wrap Context creation within a Factory.
Because dependency injection container can and is used for lifetime management. Basically, singleton's main purpose is to maintain lifetime of exactly one instance : it is created once at startup (or first use) and is destroyed when application ends. But if you put both lifetime management and behavior itself in single class (eg. singleton), you break single responsibility principle.
On the other hand, if you have a single top container and use it to manage lifetime of that one instance, based on interface. You can tell you DI container to handle that one implementation of interface as a singleton. So it returns same instance every time it is requested. There are also different lifetimes : per-thread, per-(web)request, etc..
If you use DI instead of static field, then design around this single instance becomes much cleaner. It will also make it easier to change your mind later on, if you realize that what you first thought was a singleton actually isn't.
Best Answer
The service type is defined at compile time. This is normally in the form of an interface or a (sometimes abstract) base class. The key point here is Liskovs substitution principle
So we know the contract of the type at compile time
When implementing DI, for example, using an Inversion of Control container the implementation type is not known until run time.
Let's look at a basic example of making a cup of coffee using Simple Injector as the Inversion of Control container and NUnit to test the code.
Firstly we define the cup and our service type:
And an implementation of the service:
Our test method configures the container to return the
CoffeeService
when asked for an instance ofICoffeeService
We decide we need to always add cream to our coffee but instead of changing
CoffeeService
we instead Decorate it with the new feature (Open/Closed principle).The decorator
And the new test
The most important point to take from this example is that we code to and compile against the service type
ICoffeeService
and the container gives us the configured runtime implementation. In test one we get an instance ofCoffeeService
while in test two we get an instance ofCoffeeService
that is decorated with an instance ofAddCreamDecorator
- in both cases the caller doesn't care as it get the service it needs.