10 ms·
I agree that dependency injection and dynamic scoping are similar and I think they are both anti-patterns. It doesn't make sense to stub out dependencies in un
by cryptica 7y ago
I agree that dependency injection and dynamic scoping are similar and I think they are both anti-patterns.
It doesn't make sense to stub out dependencies in unit tests (unless you absolutely have to). Stubbing out dependencies is like stubbing out native functions, operators or loops. They are called dependencies for a good reason; because your class depends on them and assumes that they work. Trying to make dependencies substitutable is overengineering and leads to poor design and gives you more work when implementing unit tests.
Dependency injection is particularly bad because it makes it difficult to find the path to the dependency's source code (which is critical for debugging). Hiding the source path of a dependency is a terrible anti-pattern. When it comes to programming, there are few things more horrible than not being able to determine where some buggy piece of logic is located. I cannot imagine any use case where that would be a fair tradeoff.
Any kind of injection of dependencies should be done via an explicit method/constructor parameter. Sometimes it means that the dependency instance has to traverse a few classes in the hierarchy, but that's way better because at least you can unambiguously track where the dependency came from. Also, if the dependency has to traverse A LOT of classes, then you know there is probably something wrong with your architecture (e.g. a dependency imported too high in the hierarchy and used in a deeply nested class can indicate poor separation of concerns and leaky abstraction between components).
- deleted 7y ago[deleted]
- hu3 7y ago> injection of dependencies should be done via an explicit method/constructor parameter. Yes! This functional approach is so simple yet so powerful and magicless. I've recently refactored a small 20k LOC codebase and the first thing I did was to ditch some magical dependency injector in favor of passing dependencies as parameters to functions. Often dependencies are functions themselves making it straightforward to compose and test.
- nurettin 7y agodoes that mean you went from def send_alerts(): for person in everyone: Mail.send(type="alert", person) where Mail.send calls an object that was created with Mail.create(env=os.environ["ENV"]) to def send_alerts(mail: Mail): for person in everyone: mail.send(type="alert", person) where send_alerts is called as send_alerts(Mail.create(env=os.environ["ENV"])) ?
- domlebo70 7y agoNot the OP, but yes I imagine that's what they did. But Mail would probably be created further up (probably at the top level in the main function), and passed to send_alerts, rather than created inline. I use this approach in all my Haskell/Scala code, and it works really nicely. I have never once felt the need for some magicy DI framework. I just pass the dependencies via function arguments. If it becomes too bloated, I group dependencies, or rethink my design.
- ryeguy 7y agoDoesn't that mean you need to bubble up every dependency up the stack? Like if function a calls b which calls c which calls d, and d needs a new dependency, wouldn't that involve modifying a, b, and c's function signatures and all call sites?
- hu3 7y agoYep it does require a little more work when bubbling dependencies. However it's less of a hassle that it might seem since changing dependencies is less common than I initially antecipated.
- nurettin 7y agogroup dependencies as in def send_alerts(services): for person in everyone: services.mail.send(type="alert", person) ?
- domlebo70 7y ago
- caseymarquis 7y ago'Trying to make dependencies substitutable is overengineering and leads to poor design...' I absolutely agree with this[1]. However, DI does not require this. You can do DI without substitution by default, you can still easily do unit testing, and you can make dependencies explicit. To be fair, I had to build my own framework to make this work exactly the way I wanted, but I also wanted a built in actor framework. I don't have examples of how I do testing, or other features I use, but you can get a feel for explicit injection by default from the readme I made: https://github.com/caseymarquis/KC.Actin https://github.com/caseymarquis/KC.Actin I'm sure there's some established DI framework which uses concrete types by default, and substitution when required. If not, it's not rocket science to roll your own. 1. Substitution by default is useful when you're building a library for mass consumption, but libraries typically don't need DI.
- pjc50 7y ago> Dependency injection is particularly bad because it makes it difficult to find the path to the dependency's source code I agree. To me it seems that the problem is globals. In the Java example, it's convenient to think of "the bank" as global, right up to the point where it isn't (need to substitute a test service, or different banks for different regions etc.) DI replaces global variables. "Magic" DI where this is handled invisibly by a framework lets you pretend it's still a global variable, with all the advantages and disadvantages of that. I too prefer "explicit" DI where the dependencies are passed in. This does tend to lead to constructing a "world" object which carries around all the pseudo-global variables. Perhaps this would benefit from language support in some way, just as Perl has $_ to allow non-explicit parameter passing.
- mcphage 7y ago> Perhaps this would benefit from language support in some way, Scala has implicit parameters, which are declared explicitly in your method signature, but aren't passed explicitly into the method call (so long as exist in scope as an implicit where the call occurs). They can be confusing, but they can also help.
- scarface74 7y agoI keep seeing posts about wanting “explicit” DI instead of “Magic” DI. The article’s author only showed examples of using constructor injection. No one is arguing that it should be done any other way.
- UK-Al05 7y agoMost DI frameworks you explicitly pass things in via the constructor. I don't get it.
- kixiQu 7y agoThere can be some magic with jpassing
- mateuszf 7y ago> Any kind of injection of dependencies should be done via an explicit method/constructor parameter. I would go further and always recommend manual DI - that is passing the objects and creating them in code rather than depending on some magic framework to do the job. That makes understanding / analyzing / fixing bugs a lot easier.
- UserIsUnused 7y agoI like dependency injection as an assemble mechanism in OOP languages like java, so that I just don't have to go around with "news" everywhere constructing every thing I need. I don't like having to use mocks and stubs, and I think that using those everywhere is indeed an anti-pattern. Having interfaces for stuff that won't really have polymorphism, it's just there for DI or because some guy likes interfaces is just wrong IMO. Interfaces is a layer of misdirection, if you don't have a use for it, don't use it. If eventually a use comes, then you put an interface there, with modern IDEs, there's even short-cuts to extract an interface from a class and do the renamings if you want. So please don't make "interfaces by default" that some DI apologists seem to love.
- UserIsUnused 7y agoI would also like to add that I do love Spring's new functional bean definition for kotlin, keeps my kotlin stuff cleaner, and it's easier to replace if I get fed-up.
- mateuszf 7y ago> Any kind of injection of dependencies should be done via an explicit method/constructor parameter. I would go further and always recommend manual DI - that is passing the objects and creating them in code rather than depending on some magic framework to do the job. That makes understanding / analyzing / fixing bugs a lot easier. It's actually not that hard / boilerplate-y to do even in Java (which is what we do in our team).
- username3 7y agoDo you have code examples of this?
- mateuszf 7y agoWell, you just create an object and set it as a field / pass it via constructor to the object that uses it. There are dependency injections frameworks, but the term dependency injection doesn't mean injecting automatically, it's more general. It just means that the dependency is provided from outside, and not how it's done. https://en.wikipedia.org/wiki/Dependency_injection https://en.wikipedia.org/wiki/Dependency_injection
- jcelerier 7y ago> I would go further and always recommend manual DI - that is passing the objects and creating them in code rather than depending on some magic framework to do the job. That is so weird. The reason why we have DI and DI frameworks is because we are asked in requirements to make things configurable out-of-code (and at runtime), because someone wants to use a pretty UI or a config file of sorts to set up which behaviour is used to do $thing.
- dvdcxn 7y agoWhat's weird about it? In practice it just means writing classes which instead of newing things inside the constructor, allows them to be passed in by the caller. This is a pretty fundamental aspect of polymorphism and composition.
- brlewis 7y ago> Any kind of injection of dependencies should be done via an explicit method/constructor parameter. In this article, that's the only kind of injection mentioned. I realize a lot of the times people talk about DI they're referring to magic injectors like Spring's, but in this article, and in the Wikipedia article on DI, DI and magic are not synonyms. When the injector is just the constructor that's still DI, but it isn't an anti-pattern.
- _pmf_ 7y ago> Any kind of injection of dependencies should be done via an explicit method/constructor parameter. These kinds of statement make me question whether you have ever worked with any framework that is not under your control.
- scarface74 7y agoYes, and sensible frameworks use constructor injection -- see ASP.Net Core.
- protonimitate 7y ago>Dependency injection is particularly bad because it makes it difficult to find the path to the dependency's source code Can you expand? Not sure I understand. Is it more difficult because the dependency is separate by an abstraction layer? Imo if DI is done right, the dependencies are always explicitly injected so there shouldn't really be any confusion about what the 'source' is.
- PeterisP 7y agoThey're explicitly injected somewhere. That somewhere often is distant both in time and space from where the dependency is used - for example, if the injection happened when that object was constructed as in the example which the original article used. If many injections are possible, then I'm in a debugger breakpoint in the code where it's used, I don't have access to the call stack of how, where and why the object was injected with that dependency. If it's usually just "the one" dependency except when testing, then if I'm looking at the code of a particular function, I don't have an easy way to see the code that must always get executed there because it "could" be (but isn't) anything.
- rumanator 7y ago> It doesn't make sense to stub out dependencies in unit tests (unless you absolutely have to). The long string of assertions that follow this remark are quite wrong and misses the whole point of unit tests. You are expected to write tests that test a specific unit of software, and that unit alone. You are expected to write tests that provide well defined input parameters to your unit of software, free from side effects, and you are expected to put together combinations of those input parameters that enable you to cover an adequate amount of code paths within your unit of software. By far the best and easiest way to pull this off is by mocking interfaces. When you start testing multiple units of software interacting or writing tests that rely on specific behaviors of external units of software them you're already testing one level above what you're supposed to be testing in the test pyramid: integration tests.
- bmm6o 7y agoExactly. I don't want to assume gp doesn't understand the difference between unit and integration tests, but I think the only alternative conclusion is they don't think there is value in unit tests. I disagree, and happily inject mock objects for testing.
- deleted 7y ago[deleted]
- UK-Al05 7y agoThe problem with unit tests + heavy mocking is that you essentially makes any sort of class reorganisation as a massive pain. I try for sociable unit tests(it's a thing), which classes that are expected to work together 100% of the time are tested together. Mock out the external dependencies(http etc) or things that are expected to change. I tend to only mock out/simulate primary ports(inputs, such http requests), and secondary ports(databases calls) for simple web apps. I may start isolating stuff for testing purposes when it can be used well independently such a piece of middleware, or things that can be used by multiple parts of a project. The main difference socaiable unit tests, and integration tests is that traditional integration tests still talk to the db.
- 7y ago
- scarface74 7y agoMany third party APIs that your code may depend on are either rate limited, cost per use, brittle, etc. if you run tests before check in or once per integration build, you can easily end up incurring costs or have unreliable tests and false positives. Slow tests are tests that aren’t run. Having unit tests that depend on databases and APIs are slower, increase the deployment cycle and usually end up being skipped. Trying to make dependencies substitutable is overengineering and leads to poor design and gives you more work when implementing unit tests. How is it a unit test if it can fail due to other dependencies? Hiding the source path of a dependency is a terrible anti-pattern. When it comes to programming, there are few things more horrible than not being able to determine where some buggy piece of logic is located. I cannot imagine any use case where that would be a fair tradeoff. If you are only using dependency injection to enable unit testing, finding the real implementation is trivial - it’s the only concrete implementation of interface to the constructor. Any kind of injection of dependencies should be done via an explicit method/constructor parameter. Sometimes it means that the dependency instance has to traverse a few classes in the hierarchy, but that's way better because at least you can unambiguously track where the dependency came from. Again if you have only one concrete implementation and you’re using a DI framework, finding the implementation of an interface is trivial with any halfway decent IDE.
- anon73044 7y agoA "unit test" that relies on a database connection, isn't a unit test, rows from a table can be mocked. Data from all external APIs can be mocked. If you want to run a full suite of integration tests, be my guest, but none of those will ever be unit tests.
- scarface74 7y agoWho are you arguing with? Does anything in my reply imply that I think it's good idea not to mock dependencies?
- jillesvangurp 7y agoMy experience is different. Inversion of control is a pattern that works just about anywhere. Javascript, C, Kotlin, Ruby, etc. You name it. I've dealt with a lot of code bases where writing tests was hard because the developers did not understand this simple design pattern. E.g. a lot of javascript frontend code ends up being hard to test for this reason. Anytime you mix object creation and logic, you make it harder to unit test. In Java, using frameworks for this is common mainly because it has reflection and annotations, which means you don't actually have to manually call a lot of things to get your objects. E.g. in Spring, all you need to do is slap the annotation @Component on your component classes and it sort of self assembles the object graph from just that information. It's kind of neat if you do this properly. There are tons of ways to customise that or do it differently but it can be pretty minimalistic. You can do this without frameworks as well (lookup DIY dependency injection). It basically just means that you write the code that constructs your objects yourself and do the right thing of not putting that in the wrong place (which results in hard to test code). There are only two simple rules you need to remember: constructors must not do work (like constructing other objects or initializing some middleware) and all dependencies come in as constructor arguments. If you do that consistently, you are doing DIY dependency injection. Any time that looks tedious, you are likely violating some of the SOLID principles (e.g. because your constructor has 10 dependencies and the whole class suffers from poor cohesiveness). In Kotlin, the trend is to move away from using reflection or annotation towards using more explicit DSLs with helper functions that figure out how to construct stuff at compile time (by using the type system and some language features). For example, KOIN https://github.com/InsertKoinIO/koin https://github.com/InsertKoinIO/koin is a framework for Kotlin that uses neither reflection nor annotations. The same principles are used in Kofu, which is a kotlin centric way of doing similar things for Spring. Aside from being easier to debug (both of these use simple function calls), it also has the side effect of enabling better startup performance as well as native compilation using e.g. Graal. Whichever way you do this, IOC ready components are easy to unit test because nothing gets constructed as a side effect of constructing them. Side effect free construction might be a better term. I was recently working on some python code that was the opposite where db initialization happened as a side effect of importing some module that exposed a global variable. I would call that broken by design. Python has all the tools you need to do better than that. There's no technical need to write broken code like that. I've seen similar problems in javascript code bases for frontend as well as node.js. Usually if you find a system that is hard to unit test, this is the root cause.
- commandlinefan 7y ago> dependency injection [is an] anti-pattern. > injection of dependencies should be done via an explicit method/constructor parameter The second sentence seems to invalidate the first (and I agree with the second but not the first). If you're injecting dependencies using language features, you're still doing dependency injection - and doing it a lot better than the useless Spring "framework".
- philwelch 7y ago> It doesn't make sense to stub out dependencies in unit tests (unless you absolutely have to). Stubbing out dependencies is like stubbing out native functions, operators or loops. They are called dependencies for a good reason; because your class depends on them and assumes that they work. Trying to make dependencies substitutable is overengineering and leads to poor design and gives you more work when implementing unit tests. This doesn't really make sense when it comes to web services and distributed systems. Often you can mirror a service dependency with a code dependency--whether that code dependency is a proper client library or just an ad-hoc client module that encapsulates the particular service calls you have to make--and then inject that dependency. It's really valuable to be able to stub those out if you don't want your unit tests making actual service calls. > Dependency injection is particularly bad because it makes it difficult to find the path to the dependency's source code (which is critical for debugging). ... > Any kind of injection of dependencies should be done via an explicit method/constructor parameter. In my relatively limited experience with Spring, explicit constructor parameters are the idiomatic mechanism, combined with annotations. And when you follow this idiom, it's actually pretty easy to trace back to the dependency in IntelliJ.