Hacker News new | ask | show | jobs
by ivanche 1535 days ago
What do you suggest instead? new operators everywhere? Service Locator? Global Singletons everywhere?

Honestly, I don't know how you came from "my object needs objects A, B, and C to work" to the "it's cognitive overload". Your object still needs A, B and C to work, just with any other approach it's implicit which is spooky action at a distance in its finest and way more cognitive load.

2 comments

The answer is that your "composition root" constructs the objects using normal code. Usually the composition root is the `main` function.

    def main:
      a = ...
      b = ...
      c = ...
      my_object = MyObject(a,b,c)
      my_object.do_something()
If you think it's ugly in `main`, then extract to another function:

    def main:
      my_object = make_my_object() # constructs my_object as above
      my_object.do_something()
I've used this pattern extensively in Kotlin and also php (ew). It works well in pretty much any language.
If a, b and c are MyObject's "dependencies", that's dependency injection. If MyObject has no other dependencies (produces no side effects other than on a/b/c), it is fully dependency injected.

Magic, monkey-patching mess of dependency injection libraries is terrible.

Dependency injection as a concept is simple functional approach to imperative and OO programming.

> my_object = MyObject(a,b,c)

> If a, b and c are MyObject's "dependencies", that's dependency injection.

In practice, though, at least coming from a Java/.NET background, DI would be understood as using something like a decorator/annotation/some registration mechanism to let the framework handle giving you a valid instance of the class that you're after, rather than straight up using the constructor whilst filling out the parameters manually.

While your example is technically true, it wouldn't be the answer that anyone in a programming interview (at least for those languages) might expect you to provide as an example.

That said, adopting the composition root approach in a Java project (or using a god-object for all of the services) was oddly liberating and felt way more simple than mucking about with appeasing Spring's @Autowired annotation and its finnicky nature.

Like most fields of endeavour, programming has gotten infested with "smart" speak and too much terminology (let's talk the rest of Design Patterns next :D), and this same terminology changes even between different teams and not just different companies or technologies. Basically, people have a tendency to want to name pretty simple things names just so they are something special, even if they need to re-explain it every single time (there is no value in such a name, imho).

The fact that most of that terminology is misunderstood simply supports that claim above. The fact that interviews are also generally silly is besides the point too. :)

Still, none of that should be a benchmark for what something is. Dependency injection is simply about injecting a dependency instead of using it as a side-effect. You can achieve that in many ways, and the simplest is just to pass it in.

I agree with you completely! As the matter of fact, I prefer this approach. But that is dependency injection at its finest, and something that NewMountain above said is "cognitive overhead" and "indirection".
I can't speak for NewMountain but I assume he was talking about using a DI framework or container (e.g. Spring) where dependencies are defined declaratively and the framework handles the instantiation. This is what brings the indirection and the cognitive overhead.
I'm catching up on this thread now, sorry for the delay. You're absolutely correct. My function needs A, B and C to work is valid. That's perfectly fine.

    if __name__ == "__main__":
        a = os.getenv("foo")
        b = get_from_somewhere()
        c = "some_hardcoded_variable"
        my_app(a, b, c)
Is what I would expect in any reasonable codebase. The indirection and cognitive overhead was taken from Java where seeing @Autowired or @Inject, hitting reverse search and not being able to keyword search for the thing being injected is nightmare fuel. That is not something I would wish on an enemy, let alone on any user of a programming language I care for.

My original reply was in response to "please don't make Python Java", where that flavor of spooky action DI seems to be common.

To your original question: `what do I suggest`. I point to the authors example:

    di = Di()

    @di.register('A')
    class A:

        def __init__(self, di: Di):
            pass

        def action(self):
            print("Hi from A")

    @di.register('B')
    class B:

        def __init__(self, di: Di):
            self._di = di
            self._a = None

        def run(self):
            self.get_a().action()

        def get_a(self):
            if self._a is None:
            self._a = self._di.get('A')
            return self._a

    @di.override('A') # replace class A definition with a new one
    class C:

        def __init__(self, di: Di):
            pass

        def action(self):
            print("Hi from C")

    di.get('B').run() # prints "Hi from C"
    di.remove('B') # removes the B dependency from the registry
My suggestion: please not that.

How about:

    class A:

        def __init__(self):
            pass

        def action(self):
            print("Hi from A")

    class B:

        # A is default for some reason
        def __init__(self, a=A()):
            self._a = a

        def run(self):
            self._a.action()

    class C:

        def __init__(self):
            pass

        def action(self):
            print("Hi from C")


    if __name__ == "__main__":
        if os.getenv('ENV') == "PROD":
            a = A()
            b = B()
        # test/non-prod/whatever version
        else:
            a = A()
            b = B(C())

As someone who's been writing python for a while, I could walk into this having never read it before and say...ok sure, when prod, B takes A behavior, but when not prod, B takes C behavior.

In the proposed example, I would go in, read it (assuming I know to even look for this non-idiomatic use of the language) and then read through this code to figure out what should have just been an argument passed to B.

Which leads back to my original question: what is this buying me over dunder main or handing some arguments? In terms of cost, I have to understand something and walk through the code to understand something that could have been done more simply and more explicitly without this DI pattern.

Edit: formatting

Thank you for very detailed reply! As the matter of fact, I mostly agree with you and prefer such a direct (or manually wired) approach to DI, over Spring's declarative one. Somehow I was under impression that you are against DI as a concept. I am sorry for that.