Thanks. I skimmed the slides, it looks like he's saying three things. 1 is that dependency injection is doable in a few ways (he didn't mention mine) with Python, but it's kind of tacky. 2 is that what you really want is functional programming so that you don't
need to do dependency injection at all, or at least not on as many things, the dependency is just data and pure functions won't change output with the same input (so can't do I/O which is one of the bigger common dependencies to factor out)! (Reminds me of Clojure's value of values.) Tests are easier with functional programming (and not stated but I like that it encourages doctests too). 3 is that the "onion architecture" (I think this is the same thing as the "hexagonal architecture" which you can find around the internet with pretty high levels of praise) is great and helps with this and other goals of good design. All 3 I agree with, though only in principal on #3 because I haven't had a chance to try it all the way in a large system, and I feel like it'd be a lost cause trying to bring it into an existing legacy system without full team support. We just need to get our crap under test first so the fires can die down, worry about better architecture refactoring where testing can fall out naturally later.
I came from Python but have worked a lot in Java (and primarily in Java for the last few years). It took me a while to accept dependency injection but now I see it as fundamental to good Java design. But I don't see the same for Python. My reasoning is that it comes down to the difference in static and dynamic typing and how imports work. Maybe even mostly on how imports work, since DI did take off in Angular for JS, but if JS had a module system it might not have needed to like we see with Python. Ideas that quickly spread and cross quite different language boundaries are the ones I pay more attention to (like the industry shift to more functional designs and patterns, "composition over inheritance" when OOP is still in heavy use, and so on), DI hasn't been one of those.
The canonical example of dependency injection in Java is having a method that constructs a new Date object (or something that is based on the current system time) and does something with it, and asking the question of how you would test it. Well the JVM is pretty dynamic under the hood so if you have to (useful for legacy systems or third party code you can't really modify) you can cheat with something like PowerMock and overwrite the bytecode for the built in Date class with your mock one. But normally, such usage is frowned upon, just change the code under test. So DI comes into play where you bring either the Date class or some common time-related interface up into the method signature argument, and you provide the value in your test. You may or may not have another method for prod code to call that supplies a default value, depending on whether you want the burden of giving a good default (if such a thing makes sense for the use case) to be put on the client or not.
When you have dependencies that your whole class uses, then it's common to put those on the constructor so clients will pass you in the object, instead of you making one yourself. Frameworks like Spring make that slightly easier to work with (especially when you get into cross-module service classes you want to depend on) by letting you just specify a setter() and it will magically make sure it autowires an object for your class methods to use when needed. You can easily resolve circular dependency problems this way too, A <-> B is resolved by having two more modules for IA and IB that each of A and B depend on instead with something behind the scenes to make sure it all wires up correctly. Interfaces are great since now you can depend on much less. If you want a mock, just implement a test class implementing the interface. You can use the less powerful Mockito library to help only mock out the parts of the interface you actually need to test (but this implies access to the source so you can know such things about the implementation). With interfaces though you can in principal even test while treating the function as a blackbox, and if you can prove that you've passed all possible states for the dependency interface you can prove interesting things. Or just fuzz it. Or be allowed to have your IDE helpfully autocomplete types and suggestions.
Python doesn't have interfaces, being duck-typed, nor does it force static typing. This alone makes DI less useful. I can write:
def unix_time(time_dependency):
return time_dependency.time()
and have it live in its own file, nothing else in it. Java would throw a fit, it needs to know what type time_dependency is. Python doesn't care, as long as it can call the .time() function on it. I think this is great, but some programmers are terrified. But anyway it's following DI and assuming time_dependency.time() is pure (it may or may not be) would make unix_time also pure itself. But it's not very helpful if you're looking at the signature or even the full source itself. Something more helpful would be:
import time
def unix_time(time_dependency=time):
return time_dependency.time()
Then you at least have an idea what to pass it yourself besides just something that can have .time() called on it, or you can pass it nothing. Something you would see in Java, but with the same method names instead of the different ones that Python requires:
import time
def unix_time_internal(time_dependency):
return time_dependency.time()
def unix_time():
return unix_time_internal(time)
But the most common implementation in Python is probably this:
import time
def unix_time():
return time.time()
The Pythonista in me also thinks this is in fact the best implementation. It's just as testable as any of the others. As I mentioned in my first comment, in your test code, instead of passing in a time dependency directly, or using a mock/patch library that starts to smell like PowerMock, you can literally just call (assuming the function was in a file called timeutil.py) timeutil.time = myTimeDep and then call timeutil.unix_time() with your substitution used. This is because unlike in Java where imports are definitions for a compiler to lookup and insert things about, in Python they are global (to the file) variables, and they can be redefined, and with Python's dynamicism it looks up the name of the thing each time to get its value instead of doing it once and coding a static reference to it, so your redefinition will take precedence. Of course this pattern isn't even that unusual in non-unit Java tests, frequently the test will save the current value of something and overwrite it in the setup, run the test, and then restore the old value in the teardown. At least in Python you can have a clean decorator for that.
In your code I opened util.py at random. Looking at GetHomeDir, I can't test this in the Java sense, it has dependencies in its implementation on os and pwd. But that's ok, because unlike in Java, those dependencies are actually global (to the file) variables that I can mutate. Plus you probably tested it manually in the REPL, another development benefit Python has over languages like Java, you can build and test things bottom up sometimes. Thanks for the code sample though, I'll keep it around for future reference. (Would you say it's common these days in the community to have test files living next to the implementation files? The only place I see that these days are with JS components.)
Probably the initial resistance I had with DI is that taken to the ultimate extreme, DI would take the form of a style of functional programming whose name escapes me at the moment, but essentially one where everything is passed as a parameter, including language built-ins, and only at the top level where you define a main() can you specify all of these things that the language will inject when it runs your program.
When I work with Python, I don't see a problem with not having the same DI toolkits or seeing projects not following it very well. No more so than not having privates or a culture in love with a sea of getter and setter methods. But I can see why some might disagree, like your sibling comment, and cry out for static types as the answer. Well, there's Python 3, not sure how much it gives you for this, and there's a ton of other static languages out there, I'd suggest working with those and leaving the Python community to do its thing. Functional programming in general, though, that's the popular thing in pretty much every language in modest use at this point, it's good stuff. A lot of bugs in old Python code I wrote could have been avoided if I had written in a more functional style, probably not as many if I had followed DI more rigorously.