When an object requires other objects for its work, pass references to those other objects into its __init__ method. That’s it.
When an object requires other objects for its work, pass references to those other objects into its __init__ method. That’s it.
Consistently applied, all construction gets pushed to the entry-point of the program. Congratulations, you've just discovered the so-called "composition root".
Now that all construction is taking place at once, the order matters as you can't pass a dependency to its dependent until the dependency has been constructed. But it may have its own dependencies. So now there is a topological sorting problem.
Turns out computers are really good at topological sorting. So, someone made the computer do it, and we call that a dependency injection container. Tada.
I guess I just don't see the problem.
That solves itself naturally through normal programming. Some function takes A as argument? Well I obviously make that A first. A requires B to setup? Well obviously I make that first.
I don't need to "sort" anything, it just follows naturally from the types and constraints of the API.
Since now, I can go to the project's root and read step by step how the application is architected and build up and which are core dependencies and which are not.
With dependency injection containers, everything is shuffled around and I'm now at the mercy of a library to tell me what I want to know.
I never knew this has an explicit name.
> Turns out computers are really good at topological sorting. So, someone made the computer do it, and we call that a dependency injection container. Tada.
What exactly is a "dependency injection container"? I've searched the internet for the definition, but I've only gotten more confused by all the PHP and C#. You mention topological sorting of dependencies - does that assume a hierarchical object model or can it be used with basically anything?
EDIT:
From what I've read, it appears that a Dependency Injection Container is just an object that:
1) for each dependency type, has a `GetDependency: Type -> Object` method
2) lazily creates dependencies as required by the dependents and the dependency dependencies.
Is this accurate?
* swapping out a class for an equivalent becomes an configuration concern.
* whether a type is a singleton or transient becomes a separate concern to that class's implementation, and it can be changed in the app startup config code.
There can be drawbacks too, if done badly, but I don't think that the argument that "this doesn't do very much" is entirely relevant to the pros and cons.
I wonder why Zope died out? It has everything, it's a completely sound design, and makes all components interchangeable, and it's very pretty when done properly.
It died out because it's verbose and gets hairy quickly in the real world where not everyone is on the same page, so it never ends up being used "properly".
Go for simpler and you have a higher chance of everybody getting the intent on their first reading of code.
It's going to be hard to get people to not do DI "properly" if you simply ask them to pass dependencies in (though I am sure they still could by eg. passing classes vs instances in).
Honestly, in practice it worked and wasn't too bad to work with, which was interesting to behold - in practice everyone talks about how circular dependencies are bad for a variety or reasons (trying to print or process data and ending up with endless loops comes to mind), but then there just was that system that was chugging along without a care in the world.
The compiler generally has better attention to detail and the ability to deal with larger object graphs than the typical human.
Sorry couldn't resist. The additional dependency is the biggest problem I have with OP's approach though. Now none of my classes are easily portable across projects, and this DI framework is going to spread around everywhere.
class MyDep:
def do_foo(self):
print("fooo!")
class Fooser:
def __init__(self, foo_dooer: MyDep):
self.foo_dooer = foo_dooer
def act(self):
self.foo_dooer.do_foo()
class FakeDep(MyDep):
def do_foo(self):
print("booo!")
# With the grand unveiling:
fooser = Fooser(MyDep())
Seems a lot simpler than 100 lines they brag about, yet this includes the business logic implementation for the code as well (dependency injection takes a couple of lines total; if you add interface handling or defaults, you'll add a few lines).1. An add_foo instance method.
2. Arguments to the `__init__` method, ideally as *args, using add_foo internally.
3. Optionally, an @instance.foo decorator which inserts the dependency as it's being defined (again, using add_foo internally).