Generally I would always answer “yes” to this question, because at this point, your DI container has become a service locator, and you’re losing the benefits of DI.
However, I wonder if it is acceptable in the following specific scenario.
- I have an application with a DI container.
- The DI container is responsible for constructing instances of components.
- Upon component construction, the container resolves the component’s constructor arguments (Reflection) and injects any applicable dependencies – either from its cached instances, or constructing them anew
- Constructed instance returned to caller, and cached in the container – next time an instance of this component is requested, the currently existing one will be returned.
So really the container has two roles: enforcement of singleton pattern, and dependency management.
But this isn’t really relevant to the question; I’m comfortable with the container operation, it does its job well and I’m comfortable using it. It’s only directly interacted with during application bootstrap to create instances of core components with their dependencies, and I feel this is the correct way to be doing things.
So you won’t see $ this -> DIContainer
anywhere else in the application except higher-level orchestrator components.
Excuse the preamble, but I want to make things clear. There is one exception to the above sentence.
The application supports plugins, and naturally, we cannot anticipate the dependencies of plugins ahead of time. We have no idea where this plugin came from or what components it needs to do its job.
Initially this isn’t a problem – the DI container was built to do this job! We just call $ this -> DIContainer -> instance("MyPlugin")
and it’s problem solved.
But it’s more tricky. It turns out plugins can only be built by a special plugin factory, which takes care of a lot of boilerplate operations around getting our plugin instance – loading its definition from the filesystem, checking it’s valid for our API version, ensuring it’s permitted to be enabled, getting static configuration data to inject into the plugin etc.
So the factory should be the entrypoint for any consumer wanting to get an instantiated plugin.
Only problem is, if the factory is to instantiate the plugin, it needs direct access to the DI container to call $ this -> DIContainer -> ("MyPlugin")
.
Which means constructing a PluginFactory
instance requires passing in a DIContainer
instance, which on some level feels wrong.
Now, I’m not entirely against this approach. Here’s what I see as the advantages/disadvantages.
Good
- It’s obvious where to go to get a plugin instance, the
PluginFactory
, and you don’t need to go to two places - The plugin factory could be considered a higher-level component
- It’s not entirely against DI, but I might be pushing it a bit… we cannot possibly anticipate the dependencies of the plugins, so the only way the plugin factory can do its job is if it can access every possible dependency – in turn, the DI container itself is the only component which can fulfil that requirement.
Bad
- The DI container has been passed around, and is no longer kept secure in the application root component.
- It could be said the plugin factory is doing too much; perhaps consumers should call the factory to load the plugin from filesystem/do the boilerplate stuff, then defer to DI container to get an instance afterwards. Like requesting the factory make something, and going to the store to get it.
- Actually getting the DI container into the plugin factory as a dependency is itself potentially troublesome – if we assume the plugin factory is constructed through DI itself (it has other dependencies besides the container), then the DI container is probably going to be injected into itself, so it can be resolved back out when it comes to parsing the factory’s
DIContainer
argument – which seems extremely fragile to me.
Like so often, writing all this out has already helped me along the way, an I’m looking into further consideration of my point above – perhaps the plugin factory is doing too much. Maybe the factory only needs to get the pieces in place, so higher-level components can get plugin instances from DI.
Nonetheless I’m interested to hear thoughts in. This feels like a time when it just might be OK to make the DI container available to a non-top-level component, but there are clearly still concerns.