16 ms·
Dependency injection is dynamic scoping in disguise
- cryptica 7y agoI 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) ?
- 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.
- cousin_it 7y agoWith dynamic scoping, when you need a DatabaseConnection, someone up the stack from you still has to construct it manually. With dependency injection, the framework can construct it for you. I think maybe a better analogy for dependency injection is imports. When library A imports library B which imports library C, they can just declare that, nobody needs to assemble "new A(new B(new C()))". Dependency injection is the same thing, but instead of libraries you have stateful objects, like "a RequestHandler needs a RequestContext which needs a DatabaseConnection". Maybe these tasks could even be handled by the same tool, but I haven't seen such a tool yet.
- barrkel 7y agoImports, yes; more precisely, modules with parameterized dependencies.
- glun 7y agoAuthor here. After posting this to reddit I realized that the original title is wrong, and poorly reflects the actual point I'm trying to make. Dependency injection is not dynamic scoping, but the latter can be used to achieve the former. I'm drafting an update to better reflect this. I'm also going to pull out reader monads and env passing into separate sections and give reader monads a better treatment in general.
- donw 7y agoGood on you for coming here to clarify! :)
- mcherm 7y agoI really liked this essay -- I found it very clearly explained and it pointed out something I ought to have known but somehow had never previously realized. My only complaint is that when I went to go read through the other articles on your blog and perhaps add it to my RSS feed there weren't any other articles! This wasn't the first thing you have written... is there a place I can read some of the others?
- glun 7y agoThank you. This warms my heart to hear. However, this is the first thing Ive written outside of isolated reddit comments. But I do have a backlog with a few articles that I have mapped out but havent gotten around to writing yet.
- HereBeBeasties 7y agoPlease do. IMO, this was very well written, well-paced and interesting.
- mercer 7y agoSeconding the sibling comment that I'd love to read more of your posts, and the most likely way I'll notice a new post is if the RSS feed works :).
- 7y ago
- CGamesPlay 7y agoIn JavaScript, Jest provides this using module mocks. The code under test imports a module, but in the test environment that module has been swapped out with a mocked implementation. https://jestjs.io/docs/en/jest-object#jestmockmodulename-factory-options https://jestjs.io/docs/en/jest-object#jestmockmodulename-fac...
- larzang 7y agoI don't really see this comparison as being particularly useful. There isn't really a choice between DI or dynamic scoping, there's just a choice between application architectures, which is largely dictated by language. The Closure example works because all your foreign symbols are coming from namespaced imports, which is to say you have a Module architecture and your only polymorphism lever is altering the namespace mapping to point to an equivalent module. With Java and the like you have a Service architecture, so rather than importing symbols you're injecting services. Things that would otherwise be exported as bare functions tend to be written as classes so that they can be used as services. While some languages can support either (i.e. JS is typically modular but there are things like Bottle if you want to write JS in a service-based way), it's not typical that a given application is going to support either simultaneously. Most languages have a clear idiomatic choice that you're likely not going to want to stray from to avoid headaches integrating 3rd party dependencies and developers other than yourself.
- deleted 7y ago[deleted]
- barrkel 7y agoDI is a module system built out of classes; dynamic scoping is another way to build a module system. If you had a good composable (parameterized) module system, you'd have much less need of DI. A composable module system would scope the lookup of type names and static methods to the actual module arguments arguments the accessing module is constructed with. The problem with `new X()` vs `@Inject X x` is that the construction of X in the former has no indirection; type names are global constants. A module system provides an indirection. Dynamic scoping could also provide an indirection, because dynamic scoping lets you redefine / redirect those otherwise constant things. (DI in practice does a bunch more, like proxies to let you put data with different lifetimes (request, session) in a mixed object graph; and the fact that proxies now exist means aspect-oriented programming sticks its head in and encourages its use for things like auth and transactions. Once you go over the edge of the DI barrier to acceptance, "best practices" shift dramatically - you end up quite far from where you started.)
- mollusk 7y agoI don’t quite see the benefits of Clojure’s dynamic binding over with-redefs for tests as demonstrated here. Personally I have only used Clojure’s dynamic binding very rarely, mostly in production scenarios to override some default value from a library and not for test cases.
- FpUser 7y agoSatisfying dependency can be setting up some pointer to a function, passing an instance of already created interface, asking some factory to supply it etc. etc. As in everything in life there is no single universal answer on what is the best way to accomplish the task. What one uses in practice depends very much from problem complexity, one's experience / personal preferences, implementation language / environment features etc. etc. I think there is no real need to dwell over the subject without knowing particular context
- ceronman 7y agoIn Python's standard library there is the unittest.mock module [1] which allows you to patch functions and methods. For example: with patch('requests.get', fake.get): definition = find_definition('testword') [1] https://docs.python.org/3/library/unittest.mock.html https://docs.python.org/3/library/unittest.mock.html
- klysm 7y agoScala implicits handle this really nicely
- denisw 7y agoI really enjoyed the comparison of dependency injection with dynamic scoping, and the explanation of how the latter can take over the uses cases of the former with less boilerplate. But one benefit of dependency injection unacknowledged in this article is that dependency injection is the explicitness of dependencies: the need to pass them in forces the caller to be aware of which dependencies exist, and changes in dependencies cannot be ignored (they lead to compilation errors in statically typed languages, at least). Managing dependencies with dynamic variables, on the other hand, is implicit. It's impossible to know which parts of the dynamic environment are used by a module without inspecting its source code. And changes to the module's dependencies are not noticed by callers, which may lead to cases where tests fail to stub out particular side effects without anyone noticing. Given this drawback, dependency injection still seems like the better trade-off to me, despite of its higher amount of required boilerplate. Perhaps it is possible to bring some of the explicitness to the dynamic scoping approach, though.
- _bxg1 7y agoPlus, passing it via function parameters allows you to do things like currying (outside of the constructor case, I suppose) const foo = (env) => (arg1, arg2) => { ... } const fooWithEnv = foo(myEnv)
- eru 7y agoThat's what the Reader Monad does under the hood. See eg https://eli.thegreenplace.net/2018/haskell-functions-as-functors-applicatives-and-monads/ https://eli.thegreenplace.net/2018/haskell-functions-as-func...
- roywiggins 7y agoNow I'm flashing back to a system that passed important configuration via globals, so to call functions that relied on these globals, you had to carefully make sure that the global environment was in the right state before you called certain functions. It was awful.
- 7y ago
- jakub_g 7y ago[Offtopic info for the author] I see [1] you're using <frameset> to wrap GitHub Pages with your own domain. You could do it in a less hacky way by creating a CNAME file in GitHub repo, and updating GitHub repo settings + DNS settings of the domain: https://help.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site https://help.github.com/en/github/working-with-github-pages/... (Unless there's some particular way why you don't want it? I'd be curious. Ability to gather server-side logs?) [1] learnt it because the page fails to load with uMatrix extension so I checked source.
- glun 7y agoNo I only did it that way because I couldn't get it to work with a CNAME file. However, that guide you linked is much more detailed than what I originally read so I'll give it another go. Thank you.
- zubspace 7y ago> The problem with this style of programming is of course that we have to pass the Env around everywhere So why not make it a singleton? Or even better, make it a static class with some static properties? Yes I know, I said something evil! But before you take out the pitchforks, bear with me: public static class Master { public static ISupplyService Supply; public static IBankService Bank; public static IMailService Mailer; } This is C# and I actually LOVE this pattern. I have seen it referred to as Master-Pattern, but I don't know the correct term. It solves a lot of problems: * You don't have to pass essential modules around anymore. You can initialize the Master once and access them everywhere without caring about their implementation. * Code completion does wonders on this one. You simply type "Mast..." and it will show you ALL available modules. You don't have to remember anything. It's awesome for new team members. * It is fast. In release builds you can even try to replace the interfaces with the actual classes implementing it for a straight method call. It introduces a few problems: * You increase coupling. Once you add a module, removing it or significantly changing the interface is tricky. * You need to be careful to NOT couple modules to each other, and if you do, do it rarely. Otherwise you will have to instantiate the modules in a specific order which gets cumbersome. In my opinion: If you are a small team and have sane teammates, give this one a try. It reduces boilerplate a lot and reduces the amount of arguments you need to pass around everywhere.
- bluejekyll 7y agoHow do you test? One of the things you get with dependency injection is a method to replace dependencies with other objects specifically to do thing like unit testing with mocks or integration testing with objects that provide specific failure modes. This is much harder to do when everything is a static. Edit: some typos.
- zubspace 7y agoAll fields in the Master class are interfaces. You simply assign Mock-Implementations in the testing scenario, once in a startup routine. Done.
- 7y ago
- rb808 7y agoI prefer Service Locator to DI, Martin Fowler talked about it a long time ago - not sure if he's changed. https://martinfowler.com/articles/injection.html https://martinfowler.com/articles/injection.html
- catern 7y ago>Some examples of languages that use dynamic scoping by default are APL, Bash, Latex and Emacs Lisp. bash isn't dynamically scoped by default; it has only global scope by default. You have to use "local" to dynamically scope a variable binding.
- kazinator 7y agoThat's like saying classic Lisp isn't dynamically scoped by default; you have to use let instead of just setq.
- catern 7y agoNo. When I set the value of a symbol in a dynamically scoped by default language, it only affects the closest binding for that symbol. When I assign in bash without "local", it affects global scope. There is no way to do the corresponding behavior of "setq" in bash.
- kazinator 7y agoIt so happens that I actually know what I'm talking about in this area, with regard to both languages/families: #!/bin/bash v=xyz f1() { printf "v = %s\n" $v v=clobber } f2() { local v="abc" f1 f1 } f2 f1 f1 Output: v = abc v = clobber v = xyz v = clobber Almost line-for-line analogous Common Lisp program. Defining the variable is mapped to defparameter, local mapped to let and setq to assignment: (defparameter v 'xyz) (defun f1 () (format t "v = ~a~%" v) (setq v 'clobber)) (defun f2 () (let ((v 'abc)) (f1) (f1))) (f2) (f1) (f1) Output (from clisp): v = ABC v = CLOBBER v = XYZ v = CLOBBER
- catern 7y agoThat seems to be the case. My original statement was wrong then. My mistake - for some reason I thought the bash behavior was different.
- catern 7y ago>This should sound very familiar, dependency injection is just a more controlled form of dynamic scoping. This is not really true. First off, this is too much focus on "dependency injection", which is just one usage of a more general feature: passing arguments to functions. Is dynamic scoping the same thing as passing arguments to functions? Well, they are certainly closely related; for example, see the "implicit parameters" paper[0]. But I think it is incorrect to say that converting a function to take more arguments is "emulating dynamic scoping", any more than function arguments in general are "emulating dynamic scoping". [0] https://www.researchgate.net/publication/2808232_Implicit_Parameters_Dynamic_Scoping_with_Static_Types https://www.researchgate.net/publication/2808232_Implicit_Pa...
- msluyter 7y agoIn the java OrderService example, the author writes: "Second, we can now pass in different implementations of our dependencies when executing in test3. This is very good, but let me rephrase that in more general terms: the values associated with certain names are now dependent on the environment in which we are executing. This should sound very familiar, dependency injection is just a more controlled form of dynamic scoping." This seems slightly off to me, unless I'm misremembering my java. In the OrderService example, the value of the variable `bank` cannot change because it's a reference: it'll always refer to the same object. However, if the instantiated `BankService` is mutable, then the internals of that object could change in various ways. Hence, in practice, the dangers of this pattern only seem problematic if dependencies have mutable state. Back when I was doing java, we used spring beans everywhere for this sort of thing and iirc, they had no mutable state. In Python, I use a similar pattern a lot, where I have classes that are in practice 'immutable once initialized' -- though of course, in Python you could always mess around with the internals at runtime -- which are segregated from classes or objects that have mutable state. (Similar to the structure described here: https://medium.com/unbabel/refactoring-a-python-codebase-using-the-single-responsibility-principle-ed1367baefd6 https://medium.com/unbabel/refactoring-a-python-codebase-usi...) Of course, I get that you can't in practice know how stateful everything in your dependency graph is. But I think the real problem here (if there is one) isn't explicit DI in the form of dependency passing, but (unexpected/hidden) object mutability.
- adrianmonk 7y ago> Second, we can now pass in different implementations of our dependencies when executing in test. This is very good, but let me rephrase that in more general terms: the values associated with certain names are now dependent on the environment in which we are executing. Earlier, they established the idea that this was a bad thing by having the printGreeting() function print a message you probably don't want. The change in the value caused bad behavior. However, with dependency injection, you should be following the Liskov substitution principle. You might get different values, but they should all be following the same contract. The acceptOrder() function might get different implementations of BankService, but the difference should be opaque to it. Calling bank.chargeMoney() should work the same regardless of which one you got. The reason I bring this up is the crux of the argument presented here against dynamic scoping is, "Dynamic scoping makes it hard to figure what our program actually does, without executing it, and that’s not a quality we want our programs to exhibit." To the extent that you successfully pull off having different subclasses follow the same contract, this weakness doesn't really apply to DI (or inversion of control).
- anaphor 7y agoIn Racket (and probably other Schemes) you can do (define foo (make-parameter "some value you want by default")) (define (do-foo) (do-something (foo)) (do-foo) ; uses the regular foo (parameterize ([foo "my injected foo]) (do-foo)) And it all just works as you would expect. `foo` gets automatically reset to the prior value when it leaves the scope of the current parameterize block. You can also nest them safely, or use them in macros, and IIRC even inside threads.
- drblast 7y agoI think dependency injection can have its uses, but the way I see it used in practice it looks like someone should file a bug on whatever programming language features are missing to make dependency injection necessary. I think most code should be purely functional and unit tested that way, which means that the only dependencies are the input parameters. "Mock" dependencies used in unit tests are usually a unit-test circle jerk; most of the time you're essentially testing that your programming language can indeed make method calls through an interface and those tests are only there because you added DI in the first place. It's common to see all kinds of testing like this but nothing testing the actual functionality of the code because that's so obscured by DI or mocked out. It feels like you're implementing comprehensive testing but it's mostly just additional complexity obscuring the fact that you're not actually testing anything real. The code that can't be functional? Sure, go ahead and knock yourself out with dependency injection and IOC. It's great for not having to pass configuration and logging instances around. But it's being abused when it's all over the place and you can't look at code and figure out what it does without also looking at configuration files and startup classes and knowing how the flavor-of-the-month DI framework works.
- matheusmoreira 7y agoThese patterns are almost always working around language shortcomings. For example, factories work around the new keyword. The new keyword in Java emits the constructed type into the bytecode, making it a hard ABI dependency. So people invented factories: they hide the new keyword behind methods that return interfaces. In better languages such as Smalltalk, new is just a method that can be overridden. Singletons work around the fact only objects can implement interfaces. Classes are natural singletons and yet they are second class citizens of most languages. It is not possible to pass the class itself to code expecting some interface. So people are forced to create an object and add complicated boilerplate code to prevent more than one instance from ever being created. In better languages, classes are the same as objects, they can conform to interfaces and be passed around normally.
- LgWoodenBadger 7y agoIn Smalltalk, how would how would the consumer be given differing implementations of this "new" method? And wouldn't that just make the "new" method a factory?
- riffraff 7y agoyes it would be a factory, the difference being that you don't have to use a different syntax (`Klass.build/new Klass` would be `Klass new` in both cases)
- matheusmoreira 7y ago> In Smalltalk, how would how would the consumer be given differing implementations of this "new" method? Classes can simply override that method. The default implementation of new is: ^ self basicNew initialize And basicNew is a method that allocates and returns an instance of the receiver. So a custom implementation can easily change self to some other class, chain more messages or replace initialize with something else, add more logic before an object is returned and so on. > And wouldn't that just make the "new" method a factory? Smalltalk actually predates the discovery of object-oriented design patterns by a couple of decades so it's the factory methods that are like the new method. For some reason language designers turned it into a magic keyword and people rediscovered the fact that methods are better.
- thunderbong 7y agoThis is my favourite explanation on dependency injection - https://www.jamesshore.com/Blog/Dependency-Injection-Demystified.html https://www.jamesshore.com/Blog/Dependency-Injection-Demysti... From the article - The Really Short Version Dependency injection means giving an object its instance variables. Really. That's it.
- moutansos 7y agoOne language that actually utilizes true dynamic scoping is PowerShell. It's true that this is an extremely powerful idea that can even override imported functions from a parent scope for things like testing, but it can very much be a nightmare. It leaves the programmer completely unaware of where a function or variable is declared or if it even is declared. Imagine the situation that while testing you have overridden a piece of code that is an interface into a real data layer in a real system, and you forget to declare it in the parent scope of the test, or worse yet, declare it and misspell the name of the function you should be overriding. You accidentally call the real function and start manipulation of data in a real system. It becomes a nightmare. DI and IoC don't have these issues because they rely on explicit passing of the dependency. So like many things, with great power comes great responsibility.