Reputation: 5471
So, Drupal uses a dependency injection container (DIC), based on Symfony2, to organize its services.
Furthermore, I like to use this pattern myself (with a simpler and hand-made solution) for smaller projects.
Simplified, it looks like this:
class Container {
private $services = array();
function getService($key) {
if (isset($this->services[$key])) {
return $this->services[$key];
}
$method = 'create_' . $key;
// @todo Check if method exists.
// Call the method to lazy-create the service.
return $this->services[$key] = $this->$method($key);
}
function create_kitchen() {
// Kitchen depends on Stove.
$stove = $this->getService('stove');
return new Kitchen($stove);
}
function create_stove() {
return new Stove();
}
}
$container = new Container();
$kitchen = $container->getService('kitchen');
So far so good.
But what if I want to replace the stove with a new one, without replacing the kitchen?
$kitchen = $container->getService('kitchen');
$kitchen->cookAnEgg();
$container->replace('stove', new BetterStove());
$kitchen->cookAnEgg();
I need a mechanism to either replace the kitchen as well, letting the old kitchen instance become obsolete, or I need to let the kitchen know that the stove has been replaced, so the second egg can be cooked with the new stove.
And what if the kitchen wants to replace the stove by itself?
class Kitchen {
private $stove;
private $electrician;
function __construct(Stove $stove, Electrician $electrician) {
$this->stove = $stove;
$this->electrician = $electrician;
}
function cookAnEgg() {
while ($this->stove->isBroken()) {
$this->electrician->installNewStove();
}
..
}
}
How does the kitchen get to know about the new stove?
Are there any best practices to handle this kind of situation?
I would think of using the observer pattern, but what is the best practice of doing that in combination with a DIC ?
EDIT:
I am taging this as Symfony2, but I assume that it could be seen as a more general question that applies to all kinds of dependency injection containers.
EDIT II:
Expanded the example.
Upvotes: 0
Views: 97
Reputation: 361
That's handled with some Creational design pattern : factory or builder for example, where you pass all parameters to it that it needs in order to decide which service type it should return and how to construct service object graph.
Symfony2 has already all this implemented. I don't see a point of doing it yourself. Instead of using whole symfony2 stack you could just use that DPI component.
But if even that is too big for you, you could go with Pimple.
Writing it yourself might be interesting with lot of occasions to learn, but sometimes it's just reinventing the wheel.
EDIT
Symfony container is not made in a way so it can be modified at the production runtime, so it does not support $container->replace('stove', new BetterStove());
. That's, again, for the runtime. In the build time, it is different, and once defined service, can be redefined, but I understand you're asking about prod runtime. So, once container is finalized (built) there are no more changes to it.
So, in your example, you have a Stove()
and a Kitchen(Stove)
. After some time you add BetterStove : Stove
, and keep old Stove()
and Kitchen(Stove)
. The answer is now you would have two kitchens: one build with old Stove
and the other built with new BetterStove
.
Simple, straightforward solution, would be to add kitchen_with_better_stove service, and let the users of the container decide when they need regular kitchen, and when one with new better stove.
If the logic on making that decision, which kind of kitchen is needed, is complicated, and you don't want to repeat it all around your solution, then you can encapsulate it a factory method with all arguments required to make such a decision, and then users of the container will query for the factory, and call it's getKitchen(arg1, arg2...) witch will return Kitchen
with either old regular Stove and with new BetterStove, depending on those arguments you passed to the factory method.
Ofc, in a simple hand-made solution, you could make mutable "container" doing all kind of stuff, but as at least I understand DYC, that's not the point - container is just assembling your composites, not doing the logic for them.
Upvotes: 0
Reputation: 214
It seems to me that the point of having dependency injection is so that you can decide which stove is best before you create the service definition, and the code that uses it is completely ignorant of how the stove works.
This lets you decouple the service implementation from the calling code. (In theory, because you still have to know the interface of the service object, which the DIC unhelpfully hides from us.)
Then you can mock a stove and put it in the container for testing, without incurring the dependencies that the production stove might have.
If you switch services in mid-stream, you are no longer injecting dependencies, you're just using the container for some other pattern.
Basically: Don't do that. :-) Find the configuration that leads to a need for a different stove and handle that dependency either in *.services.yml
or in a compile pass, and/or work on making the stove better without changing its interface.
Upvotes: 1