17 ms·
In other words, use a pure dependency-injection style, and ZERO globals. I'm writing Python and some C++, and I have been doing this for maybe 5-6 years. It t
by chubot 9y ago
In other words, use a pure dependency-injection style, and ZERO globals.
I'm writing Python and some C++, and I have been doing this for maybe 5-6 years. It takes a little practice to structure your programs like this, but once you know how, it has a lot of benefits.
It's not really idiomatic in those communities either. Python and C++ Libraries use globals all the time, although I think it's becoming less common.
I guess Java programmers have been doing dependency injection for even longer. It probably became popular more than 10 years ago. But I'm not really sure how "pure" the Java ecosystem is. Singletons are banned under this style. Instead, a singleton is just an object you instantiate once in main(), and pass throughout the rest of your program.
I think the problem with Java is lack of free functions. Static methods confuse things too.
The pure-dependency injection style is basically functional programming, requiring state as an explicit parameter.
- wickawic 9y agoIf you are using spring (or any of the less cumbersome DI frameworks) this is exactly true. Generally in Java DI frameworks you make a bunch of factory methods for your logic classes(this may be magically done through annotations). A singleton in this approach is just a factory that always returns the same object.
- chubot 9y agoWhat I don't really understand is why you need a DI framework in Java. I use this style Python all the time with zero frameworks. You just make the objects in main() and make the objects in your test main(). Use helper functions if necessary. I do think Spring was very influential because I remember other people in the mid-2000's talking about testing, dependency injection, and Spring. I recall lots of people using Guice at Google but I never used it since I did very little Java. I feel like there is something about Java where you need a DI framework, and in Python you don't. I don't feel like you need one in C++ either. Or it might not even be possible to write one. I think LLVM is written in C++ in a style with zero globals. It's a library-based design.
- deleted 9y ago[deleted]
- dom0 9y agoPython has had closures for a long time, which make great ad-hoc factories that can capture state from upstream functions without making state visible along the way.
- chubot 9y agoYeah that can probably help. Another thing that helps is keyword arguments, so you avoid the verbose builder pattern that's idiomatic in Java. I was going to say that I don't use closures in Python all that much, and Python has some issues with closures (e.g. nonlocal is ugly, and it wasn't even there until Python 3). But after watching a recent PyCon talk [1], I realized that the "bound method" feature of Python is a useful special case of a closure. And he makes the good point that the code is shorter than JavaScript which requires an explicit anonymous function or bind(). [1] https://us.pycon.org/2017/schedule/presentation/768/ https://us.pycon.org/2017/schedule/presentation/768/
- deleted 9y ago[deleted]
- deleted 9y ago[deleted]
- vbezhenar 9y agoI've learned about Spring 7 years ago and I never used singleton or global variable since then except for thread-local caching (because incompetent designers decided that SimpleDateFormat couldn't be thread-safe) or something like that. Even if my application is small enough so I wouldn't use Spring or another DI tool, I'm just passing dependencies through constructor and it works just fine. I guess, many people did it long before.
- chubot 9y agoYeah as I alluded to, libraries are the thing that hamper this style. You don't control all the code in your application. But it's not too hard to work around. I also think language-supported thread locals are superfluous in this style. Instead, just create a single object that runs into a single thread. Then you don't pass the object to any other threads. The members are automatically thread locals. If you have N threads, then you have N objects confined to those threads. There doesn't need to be a separate concept of thread-local IMO.
- weberc2 9y agoYeah, this is a huge benefit, especially in a language like Go where concurrency is cheap and common.
- wcummings 9y ago>I'm just passing dependencies through constructor and it works just fine. Eventually its gets unwieldy when you have lots of classes. I've worked on old code in that style (DI w/o a library), it produces a lot of boilerplate. Piles and piles of "container" classes filled with deps getting passed around gets real old, real fast. I strongly encourage people to use a framework like guice.
- weberc2 9y agoThere are two parts to the problem. The first is that many languages (like Java) require a bit more boilerplate than others (like JSON) to express a composition of objects. The other problem is that complex applications have a very large number of compositions, so the overall pain is the product of the number of compositions and the boilerplate per composition. The number of compositions is a necessary evil--a framework won't fix it, but a framework can fix the per composition cost by moving the compositions into a cleaner language. Go solves this problem without a framework simply by having convenient syntax for composition.
- Jach 9y agoHave you found a good writeup on using Python like that? I was talking to someone the other day about this, and how it's not that idiomatic or seen as a big deal in Python especially because you can easily swap out globals from the calling / test site by e.g. setting module.Dep = MyDep so that when module's Foo class has a method bar that constructs a new Dep, it gets your MyDep instead.
- weberc2 9y agoThis drives me crazy. Rather than a uniform separation of object definition and object graph assemblage, we'll just weld arbitrary parts of the object graph assemblage to arbitrary object definitions and write a unique snowflake hack in each test case to work around it, all because we can. The lack of consistency will make the entry point every bit as illegible if not worse, but surely if we weren't supposed to write code this way, Python would make it difficult. Honestly, this is my favorite argument for static typing. Stupidity should be difficult. I say this as a Python developer. We waste a lot off time debating this in code review and debugging this junk code when it inevitably leads to bugs or inexplicably broken test cases. Static typing would have precluded these hacks (and others).
- chubot 9y agoI was going to write that I don't have a good pointer, but actually I think the idea is basically the "onion architecture" or "clean architecture". I don't recall if I watched all of this talk, but in general I like Brandon Rhodes' PyCon talks : https://www.youtube.com/watch?v=DJtef410XaM https://www.youtube.com/watch?v=DJtef410XaM http://rhodesmill.org/brandon/slides/2014-07-pyohio/clean-architecture/ http://rhodesmill.org/brandon/slides/2014-07-pyohio/clean-ar... I don't come from a Java background, but I think that many of these ideas originated in the Java community. They came to Python later (if at all), and it's a little funny to see Go struggling with the same thing. I think I came at it from more of a functional programming perspective, and the style gradually converged with what people were thinking about OOP. I'm not actually of all the details about "clean architecture" and "onion architecture", but they are definitely related to dependency injection. Basically all your effects and state can be instantiated on the "outside" in main() and passed INWARD. But I don't believe it necessarily has to be pure. I encapsulate all my state, but I don't encapsulate all my side effects, like printing to the screen. Some people appeared to like this comment, which is somewhat related: https://news.ycombinator.com/item?id=11841893 https://news.ycombinator.com/item?id=11841893 I'm not sure how much this will help, but here is 12K+ lines of code I wrote with zero globals: https://github.com/oilshell/oil https://github.com/oilshell/oil The main directories to look at are core/ and osh/. The main program imports a lot of stuff and wires it together: https://github.com/oilshell/oil/blob/master/bin/oil.py https://github.com/oilshell/oil/blob/master/bin/oil.py (There is a bunch of surface messiness, but the architecture is sound.) I've also written even larger programs with zero globals. Also, I believe that web frameworks are some of the worst offenders in terms of preventing you from using this style. I don't think I've seen a single web framework that made the handlers easily testable. I think you may be fighting against the grain in some parts of the Python world using this style... I can do it in certain projects because I'm writing most of the code from scratch, and forking some dependencies.
- dom0 9y agoWell, pure DI also has practical downsides. E.g. anything that wants to log something has to be able to access the logging configuration — so it needs to be passed into everything.
- breck 9y agoI just pass an "app" object down the tree, and the dependents can do app.logSomething("foo"). So basically just move all globals in the descendants to methods on the root app node.
- matthewmacleod 9y agoThis is also where I always end up with using DI, and it feels like cheating. Basically pretending that these globals aren't.
- breck 9y agoInteresting. How else could DI be done? I'm in the camp that 0 params to a fn are the best, followed by 1, occasionally 2, and almost never ever 3.
- SolarNet 9y agoBut they aren't! If someone comes along and wants to run two copies of the app that share some object they can. And in a super straightforward way. As a maintenance programmer, I love these sorts of things. It's the difference between weeks of work and days.
- hota_mazi 9y ago> In other words, use a pure dependency-injection style, and ZERO globals. And it's madness. With this style, all the functions expose their implementation details, every single thing they use to accomplish their task. It totally destroys encapsulation. Then you have the problem that if one of these functions decides to add one more thing, say log a message, then all its callers need to pass it a logger. And all the callers of its callers also need to pass a logger. And of course, they need to find a way to materialize such a logger in the first place. Insane advice. Dependency injection is popular for a reason.
- jchmbrln 9y ago> With this style, all the functions expose their implementation details, every single thing they use to accomplish their task. Not at all. Under the author's guidelines, a function can encapsulate whatever it wants, so long as it doesn't modify state or have side effects. If it wants to do either of those, it should declare its dependency explicitly. I don't see anything so insane about that.
- mixedCase 9y ago> It totally destroys encapsulation. Not at all. Only the initializer needs to know about the needs. Then in the object world you can use a combination of dependency injection and factories to abstract over all the details you don't want callers to know about, and you can do the same more elegantly in the functional world with partial application (AKA currying).
- baby 9y ago> Then in the object world you can use a combination of dependency injection and factories to abstract over all the details you don't want callers to know about And then you made Go a complicated and awful to read language
- mixedCase 9y agoSorry, forgot about mentioning the very language the OP was about. That was stupid. You would use the "New" constructor function pattern in this case, using a higher order function called at injection time to keep it reusable and hide everything. I also prefer keeping the struct it returns as unexported so you get full encapsulation, but golint doesn't like it. I guess this is a personal issue.
- julian_1 9y agoAnother problem with large Java DI framesorks is that they obscure (with xml and weird conventions) a fundamentally simple operation. They also often resort to state-changing property-setters versus pure constructor args. Dependency injection - is just making all the dependencies of a class/function explicit on construction, with a secondary affect that an implied ordering is needed to creating complicated nested objects. Avoiding hidden state-channels (globals) that can't be easily modified is valuable for being able decipher/reason about behavior, and for mocking test instances.
- pabl0rg 9y agoMost Java DI doesn't use XML these days. Spring projects typically use java config. There's also Guice, Dagger, and if you're using Kotlin, Kapsule is even better than the others.
- derblitzmann 9y agoI've always thought that a large percentage of OO design patterns originated from Java's strict OO trappings. Since Java doesn't allow for first-class functions, it necessitated design patterns in spite of a more straight forward constructs in more imperative or function languages. But this is likely just me and my biases. (Note: I don't hate Java, but it does feel constraining at times.)