5 ms·
I 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 w
by denisw 7y ago
I 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.
- mettamage 7y agoSounds like OpenGL or HTML canvas to me, if I'd had to make a contemporary comparison with what I know. Fun times, having bugs with OpenGL because of this. Oh, fun times :-D
- mrfredward 7y agoOh man. It's not as bad now with shaders, but I remember how horrible learning fixed pipeline OpenGL was. You'd try to write some simple code, the result would be a black screen, and you'd just keep adding glEnable/glDisable calls to your code over and over until you figure what invisible piece of global state was ruining your day.
- mettamage 7y agoOh man, I'm not sure if I'm imagining it, but that sounds like me in 2012.
- all2 7y agoThis sounds like a system we use at work. Go take a look at Tera Term. In the realm of programming languages it... works. It gets the job done for simple tasks. If you want to do anything complex, watch out.
- jetcata 7y agoI’ve worked on similar systems, never again. God objects are something that should be avoided so that code is readable and maintainable.
- roywiggins 7y agoOne common pattern in this system was that you'd first save the current environment, run the special "environment preparation" function, then the real function, and then write back the saved environment over the modified one, so that you didn't leave the environment changed after you returned. Unless of course you meant to change it. This was sometimes documented, you'd write in which globals a function expected and which it modified into a docstring. This was probably only in the top ten of the problems that this thing had, but I do remember it vividly. Making any change was like pulling out a Jenga block and replacing it without toppling the tower.
- scarmig 7y agoIt seems like this could be a language feature. The key would be an explicit declaration of the identifiers that the function expects to be bound in the calling context. So when declaring the function you optionally specify the dynamically scoped arguments (separately from regular arguments) that callers must have in scope. Doing so allows you to use those dynamic variables within the body of the function.
- erik_seaberg 7y agoScala implicits. It's a curried arg list you can omit, but the compiler checks that you have implicit values of all the needed types in scope. Or you can pass whatever you want explicitly. https://docs.scala-lang.org/tour/implicit-parameters.html https://docs.scala-lang.org/tour/implicit-parameters.html
- mavelikara 7y agoScala's implicit parameters and Haskell's typeclass instances are this, correct?
- eru 7y agoI'm not sure how you'd override the typeclass instance you are going to get easily? (Haskell typically only allows a single instance per type.) OCaml lets you swap out the equivalent of the typeclass instance. But it's a pain, because there system also means that you always have to specify which instance you want.
- whack 7y ago> 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). This no longer appears to be true, as most projects nowadays do dependency injection via frameworks like guice or spring. Instead of the caller injecting all dependencies, the caller simply tells the framework that it wants an instance of FooBar, and relies on the framework to magically retrieve all dependencies and use them to construct the instance of FooBar
- DaiPlusPlus 7y agoIn production, yes - but in most unit and integration test code projects you still provide explicit constructor parameters. Many frameworks also provide for dependency validation that can be performed when the program first starts-up - you can then extend your build process to execute this validation code as part of a CI/CD process - so while it’s not strictly speaking compiler-enforced correctness it’s still better than getting a nasty surprise in production. Aside: I‘d love to see a T4-based static DI object factory for my .NET projects which would be the best of both worlds: full static compiler-enforced dependency correctness without needing to code it by hand. It should be possible to make using EnvDTE or Roslyn. Would anyone be interested in that?
- samus 7y agoThe framework will fail to do so on startup though - thankfully, Spring defaults to eager initialization of all beans. But yes, you're in deep water when you make beans lazy or if you have no other choice than to make most things lazy for performance reasons, like in PHP.
- closeparen 7y agoDependency injection is typically used with a system to automate the wiring and take care of the boilerplate.
- glun 7y agoI think this is a very good point, but I also think good code organization can come a long way in addressing it. In my opinion, a good test suite should contain mostly module level test[1]. You stub out interactions with other modules (if you both read and write to another module you should use a handwritten test double rather than a mock) but leave your own module mostly unchanged. Perhaps you replace some configuration (like changing the DB driver to run against an in-memory database), perhaps you replace the entire peristence layer, but that should be about it. The points where your module interact with other modules and external systems can be isolated to a single file or package. Sometimes this means just aliasing a function or class, sometimes it means writing a proper facade. This makes it easy to figure out what needs to change when testing. I also don't think that this is a problem that dependency injection frameworks are helpful in addressing. Looking at constructors is not much better than reading the method implementations -- you still have to look at every file in the codebase. Manual dependency injection with handwritten or generated (ala Dagger) factories does solve the problem completely though. [1] I think proper unit tests should be reserved for functions that are computational or algorithmic in nature and complicated calculations/algorithms are rare in the domain I'm currently working in, though this would be different in other domains. You'll also always want some real system level tests, but not too many since they're darn slow.
- gwbas1c 7y ago> 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. There's a difference between a program that's industrial-strength versus a toy project. Any program that's industrial strength will require effort in strengthening, irregardless of the pattern chosen. (Honestly, I really wish dependency injection was a language feature.)
- eru 7y agoEffort in strengthening being required at all is independent of pattern chosen. Amount of effort very much depends.
- jkoberg 7y ago"passing them in" isn't really dependency injection, it's just taking arguments.