6 ms·
Can anyone explain how this is better than just importing a singleton instance where it's needed? I've never really understood the point of dependency injection
by ramesh31 5y ago
Can anyone explain how this is better than just importing a singleton instance where it's needed? I've never really understood the point of dependency injection.
- pumpkinman 5y agoIt's not. But it makes you look smart, and helps you justify your salary to other engineers. Management don't give a toss, as long as you deliver value (features and fixes) on time.
- dimgl 5y agoThis is not necessarily true. Dependency injection allows you to provide your own implementations for unit testing. I'm surprised we're still talking about dependency injection in Node.js. I made a really crude dependency injection library for Node six years ago. https://github.com/divmgl/nwire https://github.com/divmgl/nwire
- pumpkinman 5y agoBut you can just mock the singleton
- lhorie 5y agoYou can mock injected entities in tests, instead of e.g. always connecting to the db
- knuthsat 5y agoWith JS, you can easily mock a whole module implementation so to use DI so that you can mock the injected entities during testing feels unnecessary.
- eyelidlessness 5y agoI assume you’re referring to mocking like that provided by Jest? If so… I’ll add a two big caveats: 1. The mechanism used (clearing or overriding the cache) isn’t available for ESM, where mocking is significantly more challenging. 2. That mechanism is also responsible for a lot of memory leaks when any modules are stateful.
- postalrat 5y agoDoes bending your design to make it easier to mock result in better designs?
- eyelidlessness 5y ago1. It can promote simpler and easier to understand interfaces, but doesn’t guarantee it. 2. That doesn’t mean they’re “right” or necessarily “better”. 3. It depends a lot on the complexity of the code under test. If it’s really simple code—and especially if the stuff you intend to mock performs well and doesn’t mutate—making it easier to mock is probably going the wrong direction. 4. For most (>50%) things, yes. 5. But also for most things (off the cuff guess: >75%), you might be better off not mocking anyway. 6. If you (like me) prefer unit tests, you’ll probably get even better results by isolating as much of your code as possible from anything you’d want to mock in the first place.
- jitl 5y ago“Dependency injection”, especially this kind implementation, is fancy words for “calling your functions with commonly used parameters”. The language itself provides function parameters as part of the syntax of functions. Isn’t it easier and more clear to pass a dependency as a function parameter rather than mocking global state?
- bitwize 5y agoHave you ever worked in an enterprise environment? This kind of transformation happens all the time: <straightforward implementation> called <simple name> => <complicated implementation of same thing> called <fancy name> because <scalability, compliance, "best practice", or other BS reason>.
- lhorie 5y agoThe point is not so much how easy it is to mock, it's getting visibility into what should be mocked. Mocking moment.js with jest is relatively trivial, but your test is not going to warn you that your mock became inert if the implementation switched to date-fns. I've seen a number of occasions where a test using a mock passes one day then fails the next day because it turned out the mock was setup incorrectly, and the test was actually relying on wall clock, and it wasn't at all obvious that there was an issue because you could not statically analyze whether the mock was applied correctly.
- Sytten 5y agoThat is not really a good argument as you can also easily mock imported singletons in node. It is a bit dirtier but totally possible. Dependency injection was developed for languages that could not easily do that (like Java). It is just a design pattern that isnt that useful in python or js. You can decide to follow it if you think it will make your code cleaner/more readable.
- lhorie 5y ago> languages that could not easily do that (like Java) It's interesting that you bring up Java, which compiles things into class files. You can technically "mix and match" things there to achieve mocking semantics by carefully constructing jars. The point of DI is to bring composability into the realm of the language. In JS, sure you can use complex machinery to get jest to mock out imports during a hidden compilation phase, but there's no contractual relationship between the mock and the API consumption deep in your unit. E.g. I could "mock" moment.js, but if the implementation switches to date-fns, then the mock is still a valid thing, but it's no longer useful/applicable. Whereas, if there is a contract that can be enforced via types/API/DI, then switching date util implementations will trigger compile errors.
- jitl 5y agoWell said. Another reason passing dependencies as a parameter is great is to enforce constraints for a functional call. Say you have a database service that pools connections to your database. If you want to call some general function loadPermissionRecords(services, id) and be sure it only uses one connection for this invocation, you can wrap the database service with a facade that implements your resource constraint when calling it, eg loadPermissionRecords({ …services, database: database.withConnectionLimit(1) }, id) Sure, you could add the connection limit as a parameter to loadPermissionRecords, but you’ll also have to pass it down to every other function in the call graph beneath loadPermissionRecords…
- williamdclt 5y agoYou wouldn't inject Moment though, you'd inject an instance of a class (`DateManager` or whatever). You can absolutely write a mock for it that implement the same interface to enforce the same contract (`class DateManagerMock implements DateManager`), and use Jest mocking. Is it simpler/cleaner than DI? Arguable
- knuthsat 5y agoWell, similarly, you might not need that singleton instance if you just import it where needed. Maybe then it's enough to just pass the data from the singleton instance to all the things that need the data. I think dependency injection is mostly a tool to allow multiple people to work on a single codebase. Rarely would you need it when you're doing stuff alone.
- yasserf 5y agoI mentioned in other comments, but the idea behind dependency injection is mostly to be able to support multiple different platforms (AWS / AZURE / Local) without having to do anything special. Might actually write a mini blog about it to see what people think.
- eyelidlessness 5y agoI won’t say whether it’s better, that depends on a lot of things. But the point is inversion of control[1]. Some say DI promotes separation of concerns and modularity, or improves testability by making substitution easier. To me the value depends a lot on circumstances like overall complexity, likelihood of change, availability of comparable test substitution mechanisms… and more than anything magic. I don’t mind direct DI—as in, passing dependencies to things. But I generally dislike magic DI where some framework passes dependencies based on some config or usage pattern. Those frameworks are often difficult to debug and more trouble than they’re worth. 1: https://www.martinfowler.com/articles/injection.html https://www.martinfowler.com/articles/injection.html
- horsawlarway 5y agoI agree 100% on avoiding the "magic". Particularly since implementing DI in frameworks like express/koa is already drop dead simple - literally just have your first middleware add your services to the request context (Side note - this works just fine with TS, including nice auto-complete and type-hinting).
- rektide 5y agoAs someone whose walked through a lot of code-bases, I rather hate the "unmagic" just-write-whatever-nest-of-route.js-files-you-want path. It's easy to write routes in express, yes. But every company & usually every project in the company usually does it differently. Oh this person used a glob tool to find all the route.js files. Oh this person has nested route.js files littered about. Oh this project is just a massive ultra-complicated routes.js. A huge part of the win of DI & places like Java JAX-RS is that code one place looks fairly alike code another place. Dependency Injection frees us from having to know a project well, from having to be familiar with structure; we simply ask or tell, & there's a system there. I find it elegant that Express routing concerns, React concerns, and all the various configuration/providing systems you might need can be managed via IoC. Recent TypeScript example I found of these concerns, in Injex: https://www.injex.dev/docs/plugins/express#get-post-patch-put-del https://www.injex.dev/docs/plugins/express#get-post-patch-pu... https://www.injex.dev/docs/plugins/react#the-renderinjexprovider-method https://www.injex.dev/docs/plugins/react#the-renderinjexprov... There's a lot of push back against "magic", but really I think it's something that served Java extremely fantastically well, particularly the Dependency Injection/IoC systems. I think developers of those systems got a ton of value, and it was really understated just how noble & simple those worlds were, how clean & easy it was to walk up to any random controller & be able to discern more or less what data was going to get used, in what scopes. By not having all that stuff written in the ad-hoc, existentially open world of declarative code, and instead encoded as normative, regularized, low-overhead annotations, there was a kind of predictability & visualizability & simplicity that I think a lot of these practitioners probably sorely miss, now that they're probably mostly writing Node.js like nearly everyone else. ;)
- pxeger1 5y agoIMO the clearest advantage is that you can have multiple parallel instances of the same application but different services with no crosstalk. Basically the same as the disadvantages of using global variables to store state.
- donjoe 5y ago... and lots of developers forget that singletons are global state. Especially with node where you do not have a dedicated sandbox per user request, programming needs to be request-agnostic. This is especially painful with mixing server-side and non-server-side code where you do have a dedicated 'sandbox' on the client side (aka browser).
- romero-jk 5y agoIt's just a thing to make statically typed languages more dynamic. Java devs want to bring their patterns to TS/JS.
- yasserf 5y agoWas never a java dev. I just built alot of things that required to work both on with local systems (express / local disks), cloud systems (s3 / serverless) and a huge list of other things. Having my code deal with interfaces or services just works. Without this approach it would have been alot harder to build things like deepstream.io (or deepstreamHub ontop of it)
- littlecranky67 5y agosingleton are just the one service where DI would often not be required (in Js/Ts), but there are more scopes. I.e. you would want to scope creation/deletion services i.e. to a session (lifetime across http connections per single user) or http scope (lifetime of the http request from incoming request to outgoing response. In other languages like C# you might also want different "singletons" that either live in the same thread or the same process (or specifically outside of those) etc. Compared to C#, JS is not a prime candidate for DI patterns as JS has neither threads, shared memory model, nominal typesystem/interfaces or destructors (that would allow service lifecycle and cleanup). On the other hand, in C# (and AFAIK Java too) interfaces are named and versioned, and if any of your code wants to interact with those interface implenentations, they need a reference to that interface type at compile time. In JS or Typescript, you just have to pass a type that is structurally identical (which does also not allow for versioning at the language level).
- rektide 5y agoThis is a good statement on the core benefit of DI itself. Additioanlly there being well defined object lifecycles, with pluggable/hookable lifecycle points, has also been a really interesting capability that DI systems often offer. This usually goes hand in hand with a plugin system, that allows for modification of the various steps- it's easy to add logging, perhaps to wrap some behaviors in transactional or persistence concerns, or to attach some additional context to objects, all without having to modify/wrap/compose your original providers directly. I think of DI as providing a better repository of "how we make things", and a better toolkit for working with how we make things. Which enables a host of aspect-oriented programming ideas. This is not so much from DI itself, but from the systems that usually power it. There is a management of providers & objects across time & lifecycles, which makes first class something that is usually embedded implicitly in our procedural code. Surfacing these bring-up/handling/tear-down systems makes consistent, makes visible & makes malleable a wide amount of the glue-code we have to work with. Just ran across Injex the other week, which is a pretty decent looking Typescript DI library. It has a pretty minimal set of hooks (no object tear-down for example, but also almost never dealt with in js generally), but still is a good reference. It has hooks like "beforeCreateInstance" or "afterModuleCreation", to pick some random ones. These are enough to start modifying & altering behaviors of providers. https://www.injex.dev/docs/plugins#container-hooks https://www.injex.dev/docs/plugins#container-hooks An example use is their Express plugin, which allows for a kind of Python Flask/Java JAX-RS ish-ish way of writing services/endpoints. There's some nice annotations to declare what route it wants to handle. Then the plugin system deals with registering all the routes with the express server. It's all very pat, dry, & repeatable. https://www.injex.dev/docs/plugins/express/#get-post-patch-put-del https://www.injex.dev/docs/plugins/express/#get-post-patch-p... The managed repository of factories & objects has some interesting uses. In Spring, for example, there are a variety of *Aware interfaces, for watching the state of the IoC system, for seeing what objects there are, for seeing what's happening. I used this a long time ago in a EVE Online mapping application, to walk through all the things we had injected into the UI, reading out the values from the UI, and dumping them into an XML file. Then, at startup, I looked for that dump and injected all those saved values into the container. The result: the state of the UI was automatically saved at shut-down & reloaded at start up. It took some futzing around with Spring.NET but I didn't have to think about our code at all to do this: I just got spring to tell me what it had injected already, saved it to a file, and injected the file as configuration at start up. Spring made it easy.
- drblast 5y agoIt's not better. It's mostly Java-style OO masturbation that hides the fact that you're using global variables and singletons everywhere. Also a way to pretend you're unit testing everything when your code is spaghetti and includes "write to the database" intertwined with business logic instead of as a separate, composable step in a pipeline. The amount of braindead bullshit introduced by the use of DI frameworks so that people can write insanely shitty imperative code while under the illusion they're following "best practices" is ridiculous. Most of them time they're completely reinventing language features that are already there that are provably bad ideas. Don't get me wrong, DI has its place, but for whatever reason it's like heroin to the kind of people that like to feel clever and fall in love with complexity for its own sake. You know how Linus hates C++ not because of C++ but because of the kind of developer that loves C++? That.
- tarkin2 5y agoDI is a horrible mess of, generally, generated code, at least in the Java/Kotlin world. It promises dependency scopes but I find this rarely needed and if it is a lookup table suffices. It promises to create a graph of dependencies for the dependencies but I'd rather manually specify these rather than let a DI framework strangle my codebase. It promises to let you mock out dependencies in unit tests but you can do so anyway as long as your dependencies are specified in the constructors. Honestly, it was new, hip and trendy and even has its own buzzword "inversion of control", and it got to a point where you were thought to be a good developer if you were using DI, regardless of whether you actually should. It got famous, people got excited, somehow people thought only good developers used it, it's everywhere, people are starting to regret it, and some point it'll only be used when necessary (give it a few more years).
- plmpsu 5y agoIt sounds like you are doing DI without a framework. DI/IoC the pattern seems to be working for you fine if I'm reading your comment right. Manually doing DI becomes interesting/challenging when not all of the injected objects are singletons (i.e., they are tied to a certain context) or when they have a lifecycle.
- tarkin2 5y agoNo, I’m doing it with a framework, Dagger mostly. It works as advertised but the downsides makes it a huge pain. I’d rather do it manually tbf.