Hacker News new | ask | show | jobs
by zzbzq 2052 days ago
This is the common misconception/defense of DI that drives me so cRaZy I've jumped to different languages. DI containers do NOTHING, NOTHING to make Dependency Inversion easier, nor does use of a DI container guarantee (as dozens of projects I've worked on prove) that DIP is correctly adhered to.

DIP is achieved precisely when the components (be they packages, projects, classes, types) are organized with the low-level modules depending on the high-level modules rather than vice versa. In English, this usually means the I/O and any heavy framework code is kept out of the program logic.

What DI does do is confuse this matter. Class relationships which are mere IMPLEMENTATION DETAILS of a module are elevated alongside the actual component architecture of the program. The object graph becomes obscured. Program execution order becomes nondeterministic. The callstack is completely ruined, undoing decades of enlightenment since Dijkstra's "GOTO statement considered harmful." And all my experience with the pattern proves, the DI container causes programmers to not even evaluate or understand whether their dependencies are even inverted, because all classes and their relationships just become one amorphous blob floating aimlessly inside the container.

2 comments

Some fair points but I would disagree on the fundamental definition of the DIP. In my book it absolutely does not mean that low-level modules depend on high-level modules. That's just high coupling turned upside down. You have to have shared abstractions as the "loose coupling" between modules and nothing more, to say you are adhering to the DIP.

Simply, this means that no implementation module, high or low, depends on any other implementation module, ever. It only ever depends on abstractions which are shared between modules.

I don't see how using a DI container can change the program execution order, unless one is misusing it terribly - after all, its sole purpose is to provide the correct concrete implementation of a dependency to an object at the time of its construction, the execution order from the perspective of the program is 100% preserved. Sure, maybe the container itself creates my graph in a non-deterministic way, but why would I care? If my program depends on this that's just bad design imo, no amount of libraries is going to save it :)

And the object graph is not obscured, in fact it is clarified, because you look at a class constructor and immediately can see what it's invariants are! And I have yet to come across a DI library that wouldn't immediately halt and catch fire if you introduced a circular dependency chain, so it's literally not possible to have these amorphous blobs (great expression though!) in any proper DI container.

> DIP is achieved precisely when the components (be they packages, projects, classes, types) are organized with the low-level modules depending on the high-level modules rather than vice versa.

+1. This simple idea is the basis of Clean Code, Hexagonal Architecture, Onion Architecture, Haskell, Functional-Core-Imperative-Shell, among other good architecture ideas.

Page 150 of "Clean Code" says this about the DIP: "In essence, the DIP says that our classes should depend upon abstractions, not concrete details". This is very similar to pretty much any canonical definition you can find anywhere else. I'm quoting it here because the person who coined the principle is the same person whose name is on the book, so I am guessing his definition is correct.

If you're not using abstractions, you are not using the DIP. What is being described here with low-level modules depending on high-level modules is categorically not the DIP, and I am starting to wonder if this is perhaps part of the frustration that the poster we're replying to has experienced with DI, since turning your coupling upside down will have the same problems as just high coupling in general, except everything is upside down now and harder to read :)

Is it just the text that's supposed to be upside down:

    ᴉɟ (ɐ == q) {
        ɔ;
    }
Or the whole thing?

    {
        ;ɔ
    } (q == ɐ) ɟᴉ
I want to make sure I do this the right way.
Hi chunkyfunky. I recently read Clean Code, great book.

I agree with your take on DIP.

We're using Microsoft Service Collection and with the Scrutor nuget component we were able to easily decorate an implementation from another package to extend the functionality adhering to the open closed principle OCP.

DI also enables your code to be open for extension but closed for modification.

Great stuff!

Scrutor looks great, I must take it for a spin! And that is a great point about DI - being able to extend code/behaviour by injecting a different implementation of a dependency