[HN Gopher] The problem with dependency injection frameworks
       ___________________________________________________________________
        
       The problem with dependency injection frameworks
        
       Author : Vinnl
       Score  : 43 points
       Date   : 2023-01-08 11:20 UTC (11 hours ago)
        
 (HTM) web link (www.jamesshore.com)
 (TXT) w3m dump (www.jamesshore.com)
        
       | twic wrote:
       | I gave up on dependency injection frameworks a while ago. Now
       | there's just some "wiring" code somewhere that wires up the
       | components. It's small (one statement per component), trivial to
       | write, easy to understand, and makes any kind of customisation
       | easy (disabling whole subsystems under config, having alternative
       | implementations for subsystems, etc), because it's just code.
       | 
       | It's also testable! The setup code is factored in such a way that
       | it's harmless to run (eg sockets aren't opened during wiring),
       | and it does all the config parsing and resolution and so on. So i
       | have a suite of tests which run it, and then do some trivial
       | checks like "all the necessary config is available", "handlers
       | are defined for all the market data we subscribe to", etc.
       | They've caught a bunch of schoolboy errors which would otherwise
       | only have been found in staging.
       | 
       | I think anyone arguing for frameworks should spend some time
       | making a serious attempt at frameworkless dependency injection.
       | The frameworks are really doing so little for you, at
       | occasionally horrendous cost.
        
         | clumsysmurf wrote:
         | > I think anyone arguing for frameworks should spend some time
         | making a serious attempt at frameworkless dependency injection.
         | The frameworks are really doing so little for you, at
         | occasionally horrendous cost.
         | 
         | That is what I did, and decided a DI framework was much better.
         | If you have a single scope, like singletons, its pretty easy to
         | do the wiring manually. If not, then you see very quickly that
         | your scope management code and rewiring of same things at
         | different layer quickly becomes tedious, error prone (becoming
         | out of sync with another wiring), boilerplate.
        
       | oezi wrote:
       | Hard disagree. Spending design time on dependency management is
       | time not spend on more import design decisions.
        
         | justesjc wrote:
         | Are you evaluating the points made by James from your context
         | and limiting your understanding? If you work for a company
         | where software is not a differentiator, but a cost to doing
         | business, then using frameworks, DI or not, is probably the
         | right thing to do. But if your code is a core part of the
         | business, you probably don't want to give control to some third
         | party that may screw you.
         | 
         | All successful companies that I have worked for where the code
         | is core to the business, rolled most of their own software (NPM
         | for the web aside). Long term you need that control,
         | understanding and speed of change if required.
        
           | rektide wrote:
           | What major upheaval examples should we fear? DI frameworks
           | seem quite reloable, trustworthy, & consistent. I cant think
           | of any examples of a community being burned by trusting their
           | framework. I cant think of any cases or blogs where someone
           | has been left up a creek, has ended up hard clashing with
           | their framework
           | 
           | I dont see what justifies this fear, uncertainty, and doubt.
        
         | _gabe_ wrote:
         | And this is how we end up with spaghetti code. Dependency
         | management _is a critical design decision_. If you 're
         | polluting your global namespace with random classes that get
         | injected everywhere, you end up with a massive tree of
         | intertwined dependencies. Not relying on a DI framework forces
         | you to _see_ that god-awful mess of spaghetti and do something
         | about it or live with the consequences. If you 're not thinking
         | about how the major systems in your code interact and where the
         | dependencies flow, then you are missing one of the most
         | important design decisions.
        
           | oezi wrote:
           | Designing dependencies is critical but managing them is not.
           | 
           | I often found that unless you have CI you don't have
           | flexibility at all to make more than superficial design
           | changes. People spend days passing instances down convoluted
           | hierarchies because they don't have any other way. Much
           | better to use DI and start designing who needs what as a
           | direct dependency.
           | 
           | A dependency injection framework also helps you encapsulate
           | dependencies into contexts which can be used instead of
           | global namespace. At least it should if your DI framework
           | isn't just doing glorified singletons.
        
       | BiteCode_dev wrote:
       | I agreed, until I tried FastApi. DI can be done right.
        
       | chrisoverzero wrote:
       | > "This implementation is difficult to unit test." Horsepucky.
       | 
       | No, this implementation is difficult to unit test. The rebuttal,
       | "Just make a constructor [...]" _changes the implementation_. The
       | author's zeal to decry DI frameworks has made him forget for a
       | moment that constructor injection looks the same whether a
       | framework is involved or not.
        
       | majgr wrote:
       | I like these 'pendulum swings' kind of posts. In couple of years
       | this will be more widespread. Then, in another couple of years
       | somebody will invent containers for easy testing, but, in
       | meantime we will learn some useful things.
        
       | erpellan wrote:
       | Never ceases to amaze me how much passion calling a constructor
       | can evoke.
       | 
       | I have never, ever, been in a situation where calling
       | constructors was simplified by adding a DI framework.
        
         | 9dev wrote:
         | Until you want to add an argument to that constructor and find
         | yourself modifying lots of files just to update that call
         | everywhere.
         | 
         | Or need a value from the application config, and have to patch
         | the configuration instance through, several levels of classes
         | deep. After wasting a day or two with those shenanigans, you'll
         | gladly take the DI framework, which makes both scenarios a
         | single-line, 10 second change.
        
           | ars wrote:
           | Or just create two versions of the constructor - one that
           | takes the arg, and one that doesn't.
           | 
           | The one that doesn't does whatever magic you were planning
           | for the DI, does that, then calls the constructor with the
           | arg.
        
       | rektide wrote:
       | this feels like suicidally bad advice. letting Fear Uncertainty &
       | Doubt about bringing in dependencies rule your decision making
       | is... not smart. We have all been able to build great things
       | because we have relied on open source.
       | 
       | The author talks about the disadvantage that your developers jave
       | to understand larger codebases, including code you might not
       | actively be using. Ok to some degree sure. But thta codebase may
       | have countless books & blog posts about it, may have existing
       | tests and example apps that show how to work it. If you hire
       | someone, they stand a >0% of having worked eith that framework
       | before.
       | 
       | The capabilities built into these frameworks is immense. Mamy
       | jave iterated on their initial design a number of times, bringing
       | a battle-won level of coherency that DIY may not reach. These
       | frameworks often bear many modes of articulation, so that you can
       | grow & expand the festure-set of the framework younuse over time,
       | as need arises, where-as even if you do build just-the-right-
       | framework today for yourself, it may, tomorrow, lack who realms
       | of features thay could help you. For example, things like the
       | Spring Framework's "Aware" interfaces provide enormous
       | capabilities to see what's happening, and to perform subtle
       | modifications & tweaks to object instantiation or usage
       | processes.
       | 
       | The protest against magic is another messure of foolhardy
       | conservatism. It's true that, alas, many DI systems are not great
       | at helping folks understand the "magic". Visualizing & seeing
       | whats injected where, whats loaded how, often requires some
       | expertise, some knowing where to look. But there are well defined
       | rules and patterns here; it's knowable, and as a dev if you learn
       | it that knowledge can stick with you across projects & jobs. Many
       | frameworks have really good introspection capabilities- another
       | example of code you might not need in most cases, but which can
       | be enormously powerful to have when you need it. With Aware
       | classes, there is huge ability to write very small scripts that
       | make the DI runtime tell you what it's doing. Being this capable,
       | tbis flexible, tbis prepared on your own, creating your own DI,
       | seems remarkably unlikely.
       | 
       | This is such an ubuntu case. Not the distro, the meaning of the
       | word. If you want to go fast, go it alone. If you want to go far,
       | go it together. The risks portrayed here are unbelievably minor,
       | have caused real harm & damage almost never for DI. People going
       | off and cobbling together their own very partial patchwork
       | solutions have done incredible mis-service to themselves, their
       | team mates, the devs that inherit the project, the org, & the
       | customer. Use good software, adopt it, embrace learning it, and
       | dont let fear rule, dont convince yourself down out of worry.
        
         | [deleted]
        
         | rektide wrote:
         | [flagged]
        
           | benglish11 wrote:
           | I did not downvote but your original response seems like it's
           | talking about dependencies (eg. 3rd party libraries) and the
           | article is about dependency injection which is a different
           | thing. So when I first read your comment it didn't make much
           | sense to me but maybe I missed something.
        
             | ameliaquining wrote:
             | The article has a whole section about why third-party code
             | is bad.
        
       | weavejester wrote:
       | Dependency injection frameworks don't _have_ to be  "massive
       | kitchen-sink things". They can be minimal and predictable.
       | Ideally, they should just be a more declarative way of defining
       | function dependencies and execution order.
        
       | molszanski wrote:
       | I agree with this sentiment. Manual DI works well for small to
       | medium projects. I didn't want to adopt a framework for a bigger
       | one, so I've built a manual DI helper.
       | 
       | It is for typescript. It is really helpful for me.
       | 
       | https://itijs.org
        
       | karmakaze wrote:
       | > "This implementation is difficult to unit test." Horsepucky.
       | You can still have dependency injection without a framework. Just
       | make a constructor that takes the dependency as an optional
       | parameter. Done. Applause. Early lunch.
       | 
       | Ok so it's specifically the frameworks that's disliked.
       | 
       | > Furthermore, dependency injection frameworks encourage you to
       | think in terms of globals. That's what they inject! A single,
       | globally-configured instance of a class. Think about it. If you
       | one day want two different instances of an injected variable,
       | you'll need an impact driver to express just how screwed you are.
       | This has all kinds of knock-on effects in terms of reducing
       | encapsulation and separating state from behavior.
       | 
       | I would expect any decent DI framework can name things when you
       | want different flavours.
       | 
       | The only real problem I've had was with slow startup using
       | Spring/Boot which I blame on DI auto/scanning.
        
         | Traubenfuchs wrote:
         | In spring, you can have multiple beans (=DI object instances)
         | of the exact same class/interface. You can define one as
         | primary and have to give them different names.
         | 
         | You can also automate bean creation per thread, per request,
         | per session or whatever else floats your boat. Instance/bean
         | persistence is easy too, if you really want to go that far (you
         | should not).
         | 
         | For regulatory reasons, I once even had to implement a
         | datasource selector for spring, that would pick the database
         | connection based on userId.
         | 
         | Why do people that have zero idea about what they are writing
         | find so much attention on hacker news?
        
         | deepsun wrote:
         | Try Dagger, it generates DI code during build. So it's kinda
         | hard code, but the framework does it for you. So the startup is
         | much faster, and easier for JIT compiler to reason (no
         | reflection)
        
         | twic wrote:
         | The first dependency injection framework i learned was Nucleus
         | [1]. An unusual feature of Nucleus is that it has no type-based
         | autowiring. You write a little properties file for every
         | component, and to inject a component into another, the
         | recipient uses the path to the other component's properties
         | file. It is shockingly basic, but it works really well.
         | Everything is explicit, but simple enough that it's not
         | laborious to use. Having multiple instances of components is
         | trivial, because they're just separate properties files.
         | Indeed, the driving use case for Nucleus, the ATG commerce
         | framework (since bought by Oracle) had multiple instances of
         | many classes (eg the generic ORM repository class, for
         | different siloes of data). I was really surprised when i first
         | used an autowiring dependency injection framework, where this
         | is either impossible, or you have to jump through hoops to do
         | it.
         | 
         | [1]
         | https://docs.oracle.com/cd/E41069_01/Platform.11-0/ATGPlatfo...
        
       | fckgnad wrote:
       | The Dependency injection pattern is just not that great in
       | general. There are alternative patterns that are better.
       | 
       | It is not a problem with frameworks. Think about it. If the
       | pattern was good, then a good framework must exist. If no good
       | framework exists then logically it is very likely that Something
       | is wrong with the Pattern itself.
       | 
       | Anyway the reason why DI is bad is because it's too complex. In
       | your program, you should have logic, and then have data move
       | through that logic to produce new data or to mutate.
       | 
       | When you have dependency injection, not only do you have data
       | moving through logic, but you have logic moving through logic.
       | You are failing to modularize data and logic and effectively
       | creating a hybrid monster of both data and logic moving through
       | your program like a virus.
       | 
       | The pattern that replaces dependency injection in this:
       | functions. Simple.
       | 
       | Have functions take in data and output data then feed that data
       | into other functions. Compose your functions into pipelines that
       | move data from IO input to IO output. If you want to change the
       | logic you simply replace the relevant function in the pipeline.
       | That's it.
       | 
       | One very typical pattern is to have IO modules get injected into
       | modules so that one can replace these things with Mock IO during
       | unit testing. With function pipelines things like IO modules
       | should be IO functions, not modules injected into other modules.
       | When you want to unit test your function pipeline without IO
       | simply replace the IO functions with other mock IO functions.
       | That's it. I will illustrate with psuedo code below.
       | compose(a,b) = lambda x : a(b(x))         a * b = compose(a,b)
       | pipeline = IOoutput * x * y * z * f * IOinput         pipeline()
       | 
       | The above is better then:                   class
       | F(IOinputClass):              f(x) = IOinput()
       | class Z(F)              z(x) = F.f(x)              class Y(Z)
       | y(x) = Z.z(x)              class X(Y)              x(x) = Y.y(x)
       | class IOOutput(X)              print(x) = print(X.x(x))
       | pipeline = X(Y(Z(F(input))))        pipeline.print()
       | 
       | You can see the second example is more wordy and involves
       | unnecessary usage of state when you inject logic into the module.
       | (I left out the constructor that assigns the class instance to
       | state but the implication is there).
       | 
       | Dependency injection is a step backwards. It decreases modularity
       | by unionizing state with logic. It's a pattern that became
       | popular due to the prevalence of using classes excessively. If
       | you can I would avoid this pattern all together.
        
       | pydry wrote:
       | I remember the first time I used Spring and I had to debug a
       | traceback that included not a single line of code I had written.
       | It was hell. I almost gave up being a programmer.
       | 
       | Even today I work with half baked frameworks that have the same
       | problem and I _hate_ it.
       | 
       | The difference is that when something like, say, a web framework
       | does this it is buying me something _valuable_ in exchange for
       | the frustrating occasions when the magic fucks up requiring deep
       | dive debugging.
       | 
       | DI frameworks that do this buy you _nothing_ of value except the
       | paternalistic approval of people who dont have the imagination to
       | think beyond unit tests.
        
         | RhodesianHunter wrote:
         | >DI frameworks that do this buy you nothing of value except the
         | paternalistic approval of people who dont have the imagination
         | to think beyond unit tests.
         | 
         | I know I need to hop off the internet for a while whenever I
         | hit a comment arrogantly asserting such ignorance.
        
       | conradfr wrote:
       | Does he have a specific framework in mind?
       | 
       | Because most of the problems he lists do not exist in Symfony
       | AFAIK, for example.
        
       | travisgriggs wrote:
       | > Every line of code in your system adds to your maintenance
       | burden, and third-party code adds more to your maintenance burden
       | than well-designed and tested2 code your company builds itself.
       | 
       | Every SAAS vendor and framework advocate should have to put this
       | on their product in black letters in a white background. Same
       | typography as "Smoking is addictive..."
        
         | mrbungie wrote:
         | Yeah, good idea, but company architects should make a similar
         | advice about NIH though.
         | 
         | A dependency is a dependency, there may be tradeoffs between
         | using third-party software and developing new in-house code,
         | but using "Invented here" code does not vanish any kind of
         | complexity away, it just manages it differently.
        
           | travisgriggs wrote:
           | Agreed. That's why I thought the footnote in the quote was
           | brilliant. Guess I should've included that part as well:
           | 
           | > [2] Ay, there's the rub. I'm assuming competence. (If your
           | company isn't competent, well, you know what you need to do.)
        
             | erik_seaberg wrote:
             | Third-party code can benefit from industry-wide testing and
             | fixes. People we hire might come in already knowing it.
             | 
             | Competence is partly in accepting that a problem has been
             | solved and no longer needs our attention, at least until we
             | have a plan to make an improvement that will be worth the
             | effort.
        
         | barrkel wrote:
         | In a company, code you write yourself is a dead end. You want
         | as little of it as possible.
         | 
         | Staff turn over. What was a first party piece of code well
         | understood within the company inevitably turns into a poorly
         | documented piece of code written by a third party no longer
         | employed, and there is no community of users to help out with
         | problems.
         | 
         | Write and own code which is fundamental to the business model's
         | value proposition, the code which delivers product market fit.
         | Eliminate other code where possible. Upstream or open source
         | improvements that aren't part of the competitive edge.
         | 
         | There are exceptions of course, for trivial functionality whose
         | fully loaded cost of integration and upkeep as a third party is
         | higher than home grown, but it's not a lot.
         | 
         | The other alternative is to be such an awesome company that
         | nobody who contributes a lot quits.
        
       | gareth_untether wrote:
       | Zenject Dependency Injection for Unity has been an absolute game
       | changer for me. It's wonderful to be less reliant on the Editor.
       | Picking up old projects is quick because there's an easy to
       | follow structure.
        
       | dboreham wrote:
       | Dependency Injection is just a fancy, obfuscating, name for
       | global variables.
        
         | justesjc wrote:
         | Yup, seems to be a lot of stockholm syndrome here. Repeating
         | marking literature...
        
       | jupp0r wrote:
       | I like explicitly listing dependencies (as interfaces, as you
       | shouldn't depend on abstractions). Golang's context are also a
       | nice pattern for bundling them opaquely based on scope if you
       | just need to pass them through (for logging, tracing and other
       | ubiquitous purposes).
        
       | orobinson wrote:
       | > I need to know everything that's going on in my code. I need
       | simple, straightforward function calls. Nothing else! I want to
       | be able to start at main() and trace through the code. I want to
       | look at callers and find where every parameter came from. Reading
       | code is hard enough already. Magic frameworks make it harder.
       | 
       | But these frameworks aren't magic. They're just code. Sure it
       | means you have a bit more code to read through to work out what's
       | causing a problem but it's still just code. The time cost of
       | potentially more difficult debugging when things go wrong is
       | nothing compared to the time saved not having to wire things
       | together manually.
       | 
       | I also find DI frameworks actually encourage good design by
       | making it easier to write small, single purpose classes. You
       | don't need to spend time working out where to initialise them so
       | they can be passed to all the dependent classes.
        
         | pydry wrote:
         | >But these frameworks aren't magic. They're just code.
         | 
         | "Magic" in framework parlance doesnt mean hocus pocus. It just
         | means concealed abstraction.
        
           | orobinson wrote:
           | Yes I'm aware. My point was it's not that concealed once
           | you've invested the time to read the docs and peek at the
           | code of whatever framework you're using. The time to do that
           | is nothing compared to the time saved using these frameworks.
           | 
           | I wasn't sitting there thinking Harry Potter wrote Spring
           | Boot.
        
       | nomercy400 wrote:
       | The magic happens at so many levels. Apparently a third-party DI
       | framework is too much magic, but the third-party compiler is not,
       | nor is the out-of-order, speculating third-party CPU.
       | 
       | A DI framework is just another level of magic, which once you
       | accept/embrace it and play by its rules (like using a compiler),
       | makes developing other code easier.
        
       | deepsun wrote:
       | I advise to try build-time DI frameworks, like Dagger, see how it
       | generates the glue code, and then ditch it and write the same
       | glue code yourself.
       | 
       | Yes, build-time cannot do tricky cases (when it depends on some
       | runtime-only thing), but I'm yet to see any case of that.
       | 
       | Build-time is also easier on JIT compiler, as there's no
       | reflection involved in runtime, for VM it's all just hardcoded.
        
         | jillesvangurp wrote:
         | I prefer koin instead. It does no magic. It's simple function
         | calls packaged up as a nice Kotlin DSL. Pure declarative. Easy
         | to debug. It does not even use reflection. I've used it with
         | ktor, and with kotlin-js in a browser (it's a kotlin multi
         | platform library). There's basically no overhead relative to
         | the code you'd otherwise be writing manually. I'm not an
         | Android developer but I hear it's pretty popular there as well.
         | 
         | I've use spring dependency injection as well. It's actually not
         | that bad if you use constructor injection only. No Autowired in
         | any code I touch. You don't need it. Constructor injection
         | makes everything easy. And it makes it easy to test as well. My
         | unit tests do not depend on Spring. There's no need. And with
         | the recent declarative way of creating beans, it can be pretty
         | similar to koin.
         | 
         | I've done some diy dependency injection as well on occasion.
         | It's not that hard. Just separate your glue code (construction)
         | from your logic. Your main function would be a good place.
         | Constructors don't get to do work. I've seen some bad frontend
         | code that violate these rules and it's a mess where nothing is
         | testable because trying to run any bit of code you end up with
         | half the code base firing up. Lack of a framework is no excuse
         | for bad design.
        
       | rr808 wrote:
       | The alternative of wiring up your own dependencies is pretty
       | trivial and I really prefer it. Martin Fowler calls it a Service
       | Locator,
       | https://martinfowler.com/articles/injection.html#UsingAServi...
        
       | throwaway242359 wrote:
       | This is how I cope with Java DI.
       | 
       | I write my classes with one constructor that takes anything I
       | need as final private members.
       | 
       | This has the benefit of working with any DI framework without any
       | annotations or other hacks.
       | 
       | It also has the benefit of being usable or callable without using
       | a DI framework.
        
       | exabrial wrote:
       | CDI has a specification, _an extensive specification_, defining
       | the _exact_ behavior of the framework. It is not magic, it's
       | consistent, predictable, and deterministic. The implementation we
       | use OpenWebBeans, and the alternative implementation Weld, have
       | extensive extensive self tests. I don't think ever had an issue
       | upgrading over 12 years of using the frameworks.
       | 
       | https://jakarta.ee/specifications/cdi/3.0/jakarta-cdi-spec-3...
       | 
       | I would use a DI or a language that _didn't_ have a spec or you
       | would experience the things in the two articles people fear.
        
       | revskill wrote:
       | At work, all of my function has at most 3 parameters: deps
       | (dependencies), params (for parameters), ctx (for context), which
       | covers all of my use cases, easy to test, debug, isolate.
        
         | niux wrote:
         | Can you give an example of a function?
        
           | 0xb0565e487 wrote:
           | function something(deps, params, ctx) { // put something here
           | }
        
         | jiggawatts wrote:
         | This is the equivalent of a relational database schema where
         | there's only a couple of tables with columns such as:
         | (EntityId,RowId,ColumnId,Value).
         | 
         | In very rare cases this type of design is required, but the key
         | word here is "rare". It shouldn't be the norm for ordinary apps
         | such as typical web apps! If you find yourself doing this type
         | of thing regularly, then you've likely made some sort of
         | mistake.
        
           | revskill wrote:
           | You're making assumption ?
           | 
           | The code is absolutely maintainable, simple by design, simple
           | to test in isolation , simple to debug in isolation, simple
           | to scale features, simple to replace,...
           | 
           | I'm not sure what you want more for a production-ready code.
        
       ___________________________________________________________________
       (page generated 2023-01-08 23:00 UTC)