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