Hacker News new | ask | show | jobs
by xeyownt 702 days ago
Can you elaborate on what is "Dependency Injection"? Is this different from the example you show here with LD_PRELOAD?
3 comments

The topic is too complex to do it justice in a Hacker News comment, especially since it's one of those things that takes some time and practical experience for the concept to “click”.

A good starting point is this article (though it's a little outdated): https://martinfowler.com/articles/injection.html

DI is not very common in Python, for a variety of reasons, but there apparently are DI frameworks, like: https://python-dependency-injector.ets-labs.org/index.html

I've asked a similar question, along the lines 'Can you explain Dependency Injection to me, assuming I already know Haskell?'

And the answer was basically along the lines of: it's a fancy way to pass something like a function argument.

It's primarily a technique used in object oriented programming. So it's hard to translate to Haskell.

The big picture of what a DI framework does is let you declare your structural object graph using a config file or decorators and have the whole thing instantiated at runtime automagically.

The detailed view is "a fancy way to pass something like a function argument". An object that has dependencies gets them passed in ("injected") at runtime rather than calling dependent function directly or internally instantiating dependent objects and calling them.

Doing things this way in OO languages has a number of benefits, including improved testability.

It's more subtle than that. DI and DI frameworks are two separate things that often get conflated.

Injecting dependencies when you instantiate an object, or passing dependencies into a method via arguments, rather than having methods or constructors create their dependencies, is good design and increases testability.

On the other hand, DI frameworks are, IMO, an awful awful mess and mistake and I'm glad the industry has moved away from them. The problem with DI frameworks is that setting parameters automatically is only one part of it; the other part of it is object lifecycle management. After all, a Foo is automatically injected into your class, it means the DI framework needs to know how to create a Foo, and dispose of a Foo.

This is where you get into per-request sessions, context hooks, and all of the stupid Spring bean bullshit everyone has come to hate and is really the main reason for so much anti-"enterprise" software patterns.

Funnily enough, it is unnecessary. Just make your dependencies into parameters and you'll get 95% of the benefit of DI. The last 5% is the code to wire up the object graph at certain entrypoints which can be large, but should be simple code.

Agreed. I was mostly trying to give the briefest overview I could for a hn comment.

I don't love spring, but I've seen the benefits from having a well-organized, declarative graph of the top-level objects (i.e. the core objects that live for the entire process lifetime). It provides a clear pattern for how to add new code to a large, growing codebase. Without such a structure, devs end up tacking new code on in arbitrary ways.

> It's primarily a technique used in object oriented programming. So it's hard to translate to Haskell.

Yes. I specifically asked like that to avoid getting a description of DI just couched in more OOP jargon but also to provide some alternative programming vocabulary (so you don't have to try to explain everything in everyday English, which seldom goes well).

Dependency injection means that the caller explicitly provides dependencies to functions/classes rather than having the classes/function getting their dependencies from the environment.

Taking an example like the article. Lets say you have a game with a ghost which randomly moves left or right. This would NOT be dependency injected:

    class Ghost:
        def __init__(self):
            self.pos = 5

        def move(self):
            match random.randint(0, 1):
                case 0:
                    self.pos -= 1
                case 1:
                    self.pos += 1
It's constructed like this:

    ghost = Ghost()
Ghost's behaviour depends on the state of the global RNG, but that isn't obvious from the perspective of the user of this class.

So instead we apply DI, and pass a random number generator in:

    class Ghost:
        def __init__(self, rng):
            self.pos = 5
            self.rng = rng

        def move(self):
            match self.rng.randint(0, 1):
                case 0:
                    self.pos -= 1
                case 1:
                    self.pos += 1
It's constructed like:

    ghost = Ghost(rng=random)
Now the fact that the class uses random numbers is explicit, and you can pass in an alternative RNG for testing purposes.

DI is a very useful technique that can make the construction of your system understandable, and make it easy to mock out dependencies. Much like mocking however - it shouldn't be over-used. If you use DI too much your code will become opaque as you'll never know what the concrete type of code you're calling is. Python's progressive typing can help here to some extent.

-----------

Dependency injection is not to be confused with dependency injection systems, which are complicated beasts that obscure what dependencies are actually constructed or provided. They make DI implicit again with the argument that it's better because you don't have to pass parameters manually. I would argue that if you need a dependency injection system, maybe you've over-used dependency injection.

-----------

I like to think of it as being related to capability based security[1], where you have to explicitly provide your dependencies, otherwise you won't be able to access them.

[1]: https://en.wikipedia.org/wiki/Capability-based_security