Dependency injection container - Figuring out the design - A naive start
- Part 1 - Figuring out the design - A naive start;
- Part 2 - Learning the ropes in Rust.
What is dependency injection?
(Skip this you are familiar with the concept)
I find that this sentence neatly summarizes the use case for dependency injection (DI):
Making the creation of other objects on which your object depends someone else’s problem.
It is used to move the responsibility of object creation away from object user. For example, consider the creation of this simple object:
Suddenly the house user has to care about Bricks. It would be neat if we could just write:
And someone else would take care of injecting the right dependency. Now, should we add that responsibility to House? Probably not - House already has a very simple “new” method.
Instead, we should have something that can serve as a House Factory, most likely specific to our project that we are making. After all, required dependencies themselves might depend on many different project-specific parameters that we should not expose to House or Bricks. Keep them reusable and replaceable!
For example, we might want these dependant objects to be easily replaceable with fakes when running tests. Or maybe there are different deployment environments where we have to use different implementations.
Or maybe we simply want it to keep the responsibilities separated.
In the end, we can look at the Factory, and the DI container that contains many factories as the project-specific part of the system. The more generic objects like House or Bricks should be easily reusable without this DI mechanism.
Figuring out the design of DI container
I imagine this DI component to be usable at the project layer, for plugging-in existing libraries. There would be some kind of container for all object factory methods:
Yuck, a string hashmap based solution. I would prefer to avoid going over hashmap for every object instantiation. But we might be able to fix this in a moment.
Also, passing the “container”, or even simply using the “container” in factory function looks no more better than using a global. Factory should receive only those objects it can use.
First, let’s get rid of “container”, and use “args” array:
At the first glance this looks a bit uglier, but it is actually
can be much more faster: we can have some magic inside that
plugs args.get(0)
directly into "bricks"
closure. Well, almost
directly - we need to forward correctly plugged args into "bricks"
too.
This change will also allow us to separate this factory mapping from the runtime container. I will call the mapping part Registry, it is going to contain the definitions of named factory methods and factory dependencies. The actual Container will be built by inspecting and validating the Registry. It will have all the dependent factories mapped as direct calls, as well as all types and argument counts already validated. The result might look like this:
We only need one hashmap lookup to get house_factory
, and then
we can use it to create our stream of objects without any lookup.
Also, whatever house_factory
happens to be internally, it can have
any required parent factory calls locally, so also no additional lookup.
However, that args.get(0)
irks me a bit… Variadic functions anyone?
Well, no variadic functions exist. Also there seems to be no way to
invoke a closure with a parameter set. Of course, it might be possible
with some memory hack, but I can’t figure it out right now.
But.. Maybe we could use real types for factory arguments instead of “args” and infer them somehow?
For example, if I do this:
Can I find out the argument type and count and do the right thing? Quick search lends me some nice hits on overloading based on argument type and unboxed closure trait objects. I think it might be possible.
FADE OUT
Well, I wrote the above, went to implement it, and then… 2 months passed.
Two months later
Read the follow up in part 2.