Hacker News new | ask | show | jobs
by chunkyfunky 2050 days ago
"The only reason was to inject mocks". Must disagree...the reason for DI is to support the Dependency Inversion Principle : "High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces)." Won't even go into the host of side-benefits that are gained from adhering to the DIP.

In C# the idiomatic way to achieve this is via constructor injection, which is made much easier to manage in ASP.Net with a DI library that works with the framework for you (either the built in one or AutoFac / SimpleInjector, etc.). But you don't have to use a library; many times I'm writing a simple-ish console app and just use good old "Poor Man's DI" where I manually construct my object graphs at startup.

5 comments

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.

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
I'm not even a C# coder and I can't understand the complaints about this. If you want to code fast and fancy free, use Python or PHP. If you want to use enterprise patterns that will make your code clean, testable and nicely modular, then go for something like C# or Java. Why even use C# if you can't be bothered with DI because... too many lines? mindblown
Simply because not only does it not have to be like that, for years we weren't forced to use this stuff if we didn't want to.

I like C#, no, I adore C#. I have a bad memory for even the simplest method calls and the sheer power the intellisense in a statically typed language gives me to just not care means I can just code with joy.

My personal view on it has always been that someone in MS wrote MVC 1 in response to Rails on the sly. It was fantastic, a breath of fresh air into the MS constant misunderstanding of the web. Especially compared to webforms. For a while, everything was good. Then somehow 7 or 8 years ago the ASP.Net team got obsessed with ramming "best practices" down our throat and everything's gone a bit downhill from there.

You still are not forced to use it. The documentation will heavily emphasize it, but you can still manually new things up to your heart's content if you want.
This isn't even about C# at all, actually. This is about the web application framework ASP.NET Core.

And they don't even have a point in this regard, because nothing is forcing you to use the DI. You wanna make a database connection with ADO.NET and a static connection string in your Controller? Go ahead. You can do that. It's no more effort than it ever was.

You want to grow your app to run it in multiple environments and be confident you don't clobber your configuration during migrations? Use ASP.NET Core's DI.

Every app framework has some "magic convention over configuration". I personally think ASP.NET Core's "magic" is a lot less pervasive than in, say Django, or RoR. When I was first learning ASP.NET Core--coming from WebForms--there was a learning curve. You don't just throw everything into a Web.config XML bag of doom anymore, with a single, static configuration reading tool that reads just that one file. That particular magic has changed. I mean, if you want to do that, you can, just read the file yourself. But it's not wired up by default anymore. And the new way of doing things was easily learned with a couple of afternoons of reading the documentation.

Which you can do, because there is actually documentation, and a lot of it. I think part of the problem might be that people are used to other web app frameworks where the documentation is pretty lacking, so they don't even think to go looking for documentation on ASP.NET Core and think they can just jump in and figure it out. I don't think you could do that going from Django to RoR or vice versa, but people complain when they can't go from ASP.NET WebForms to ASP.NET Core without learning new ways of doing things.

Don't like ASP.NET Core? You don't even have to use it. You can (rather easily) write your own web app framework. I've done it in the past, and there are FOSS projects for doing it (https://suave.io/, https://www.abp.io/, https://giraffe.wiki/, https://dotnetify.net/, https://www.dotvvm.com/, https://coalesce.intellitect.com/).

You don’t even need to use DI with .NET... everyone complaining of DI. Just don’t use it...
You're 100% right. People that complain about DI don't understand 2 simple facts:

1) that graph of object creation is NOT equal to graph of object usage

2) if you don't use DI, problems won't automagically go away - code will _still_ have dependencies, just they'll be hidden and tightly coupled

I hope that's a typo because it's literally the opposite of what you're saying in #2.

It is precisely when you use a DI container that the problem is hidden, whether or not it's solved.

In most projects that use a DI container, the problem is usually not solved correctly, but the developers are oblivious and overconfident. They think, as you do, using a DI container means they did it right. False! The DI container only guarantees everything is hoisted up to the object constructor. This is NOT DEPENDENCY INVERSION. The objects can be in the constructor, but the dependency arrow can still point the wrong way. An obvious example of this I've seen a million times is an interface sitting right next to its only implementation. That's NOT Dependency Inversion.

If you don't use a DI container, the problem also may or may not be solved, but it is never obscured, it is completely visible.

It's not a typo - without DI (i.e. by using new operator freely whenever dev feels like) code still has dependencies, just this time hard-coded and invisible until you read the source.

Also, one can use DI without DI containers. Actually I prefer not using DI containers when possible.

That's the point I'd like to see discussed. What's wrong with hard-coding "new".
That multiple usages of the same concrete types requires changes on multiple places. For small programs, the di container itself can probably be skipped (i.e. instantiate an object graph by newing up). For larger programs, this is too cumbersome and therefore error-inducing.
> That multiple usages of the same concrete types requires changes on multiple places

Write a util function then, or like some like to call it, one of the many Factory pattern.

Yup, unit tests, and in my mind, that's bad. There's two reasons:

#1 Because the language provides no means to arbitrarily mock fields of a class, an entire design pattern and framework needs to be created just to do so. Why can't the language just have such a feature?

#2 This actually temps people in building worse design, because instead of creating pure units that don't have dependencies to begin with, it facilitates the opposite of creating deeply nested chains of dependencies, because "DI framework wires it up for me". Where if you didn't have that luxury, you'd be pushed towards pure units that don't have state dependencies, and integ tests instead, which in my mind is much better overall.

I spent maybe 15 years using C# and DI (for most of that time), and understand it pretty well. I still dislike it.

> just they'll be hidden and tightly coupled

They're more hidden with DI than without, in my experience.

At any rate, having left the .NET stack around 5 years ago, I certainly don't miss DI. My current code has more and better tests than my C# code ever did, so DI didn't really help me there (nor did it hinder me-- it was neutral). But my current code is much more explicit, direct, and a fair bit more compact. I'm definitely happier with it.

Compare 2 classes. Class A expects all its dependencies to be passed as constructor parameters. Class B has a default no-arg constructor, but buried inside 5 of its methods calls Dep1 dep1 = new Dep1(); or Dep2 dep2 = new Dep2(); Now, which class has obvious and loosely coupled dependencies? Which class has hidden and tightly coupled dependencies?

To me, answer is clear - class A has obvious and loosely coupled ones. Lo and behold, class A comes from program which uses DI. Class B doesn't.

Which stack did you move to?
It sounds like it's you that does not understand there are alternatives.
Been there, done that. Most popular alternative is new operators sprinkled throughout the codebase at will. Absolute horror!
I can see how IoC containers seem like a good idea to you then
> the reason for DI is to support the Dependency Inversion Principle : "High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).

You can still depend on abstractions without DI. Just use an interface or asbtract class in your code.

DI only plays into how you acquire yourself an instance of the concrete class for that abstraction you depend on.

So it can be given to you by your caller (DI). You can go fetch it somewhere (ServiceLocator). You can create it through a utility (Factory). Or you can create it yourself old fashion way with new.

> But you don't have to use a library; many times I'm writing a simple-ish console app and just use good old "Poor Man's DI" where I manually construct my object graphs at startup

I will support that. I'd like people to be precise in their criticism, do you find the DI pattern troublesome, or some particular DI framework?

Neither, I'm a fully paid up and card-carrying DI-club member :)

I was kind of making the same point as you did, that you don't need to use any kind of DI library to achieve the DIP, but that in something like ASP.Net it is there and simple to use.

One of the issues with solving a problem via constructor injection is that anytime you need to access a new object in the class is you need to DI it. We'll now you have to go change a 100 unit tests that are failing because of this.

I am not sure what is a better way to solve this problem, but the original OP is right - it's far too much ceremony.

Yes, this can be a problem, that is why I have come to the conclusion that unit tests in larger projects (not libraries) is mostly a waste of time, because as soon you change the dependencies of a class you should rewrite your unit test for that class anyways because the class is not really the same class anymore (it works differently), you are just keeping the class name for convenience. And there is always a danger of changing a unit test, are you sure you are testing the same thing?

I think it is ok to depend on a DI-container in your unit tests for dependencies you are not directly testing, instead of stubs or manually instantiating them, to avoid updating unrelated unit tests when some class changes it's dependency somewhere in your project.

Instead I prefer system/integration tests where you test lets say a complete request from start to finish. Now your code base can change as much as you like, dependencies can be added or removed or classes or packages can be completely deleted, the tests stays the same. And now you are having tests that much closer matches reality instead of tons of mocks and stubs that can easily fool you to believing that they represent a good estimate of reality.

By doing this way a constructor based DI does not become a nuisance instead it helps you to organize your project, as it should be. Using a DI container makes it natural to break down your classes and automatically share the same decencies between them.

Counterargument to my argument is that if you don't use constructor based DI you can rewrite your class in anyway you like and the test stays the same, that is all true, however what DI gives you is the possibility to change the behavior of a class from the outside by injecting different dependencies, this makes your code better structured and easier to reuse.

If you want to accomplish that in the non-DI case you have to go thru trouble of configurable proxy classes which just introduces one more layer of bureaucracy, the thing you wanted to avoid by not doing DI.

My own criticism of DI containers is that they introduce magic to the project, personally I truly dislike code magic, however a DI container is nothing more than on the fly factory creator. In theory you could remove the DI container from a project by manually writing factories for each execution path, for me that becomes an acceptable level of magic as long as the DI container doesn't do lots of other things that factory couldn't solve.

Edit: misspelled convenience

Sometimes I find this useful. This should also help the test errors you mention.

   public class MyTest()
   {
      private readonly IServiceProvider _serviceProvider;
      public MyTest(IServiceProvider serviceProvider)
      {
         _serviceProvider = serviceProvider;
      }

      public void MethodA()
      {
         var serviceA = _serviceProvider.GetService<IServiceA>();
      }

      public void MethodB()
      {
         var serviceB = _serviceProvider.GetService<IServiceB>();
      }
   }