[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)