Dependency injection: syntax sugar over function composition

Dependency injection, as much as it is important when writing testable, composable and well-structured applications, means nothing more than having objects with constructors. In this article I want to show you how dependency injection is basically just a syntax sugar that hides function currying and composition. Don't worry, we'll go very slowly trying to explain why these two concepts are very much a like.

Setters, annotations and constructors

Spring bean or EJB is a Java object. However if you look closely most beans are actually stateless after creation. Calling methods on Spring bean rarely modifies the state of that bean. Most of the time beans are just convenient namespaces for a bunch of procedures working in similar context. We don't modify the state of CustomerService when calling invoice(), we merely delegate to another object, which will eventually call database or web service. This is already far from object-oriented programming (what I discussed here). So essentially we have procedures (we'll get into functions later) in multi-level hierarchy of namespaces: packages and classes they belong to. Typically these procedures call other procedures. You might say they call methods on bean's dependencies, but we already learned that beans are a lie, these are just groups of procedures.

That being said let's see how you can configure beans. In my career I had episodes with setters (and tons of <property name="..."> in XML), @Autowired on fields and finally constructor injection. See also: Why injecting by constructor should be preffered?. So what we typically have is an object that has immutable references to its dependencies:

Take a file with bank statements, parse each individual line into Payment object and store it. As boring as you can get. Now let's refactor a little bit. First of all I hope you are aware that object-oriented programming is a lie. Not because it's just a bunch of procedures in namespaces so-called classes (I hope you are not writing software this way). But because objects are implemented as procedures with implicit this parameter, when you see: this.database.insert(payment) it is actually compiled into something like this: Database.insert(this.database, payment). Don't believe me?

OK, if you are normal, this is no proof for you, so let me explain. aload_0 (representing this) followed by getfield #2 pushes this.database to operand stack. aload_1 pushes first method parameter (Payment) and finally invokevirtual calls procedureDatabase.insert (there is some polymorphism involved here, irrelevant in this context). So we actually invoked two-parameter procedure, where first parameter was filled automatically by compiler and is named... this. On the callee side this is valid and points to Database instance.

That's mad! Notice that importFileprocedure is now outside PaymentProcessor, which I actually renamed to ImportDependencies (pardon public modifier for fields). importFile can be static because all dependencies are explicitly given in thiz container, not implicit using this and instance variables - and can be implemented anywhere. Actually we just refactored to what already happens behind the scenes during compilation. At this stage you might be wondering why we need an extra container for dependencies rather than just passing them directly. Sure, it's pointless:

Line above can actually be part of container setup, where we bind all dependencies together. After setup we can use importFileFun anywhere, being clueless about other dependencies. All we have is a function (Path) => Unit, just like paymentProcessor.importFile(path) in the very beginning.

Functions all the way down

We still use objects as dependencies, but if you look carefully, we need neither parser nor storage. What we really need is a function, that can parse (parser.toPayment) and a function that can store (storage.save). Let's refactor again:

Of course we can do the same with Java 8 and lambdas, but syntax is more verbose. We can provide any function for parsing and storage, for example in tests we can easily create stubs. Oh, and BTW, we just transformed from object-oriented Java to function composition and no objects at all. Of course there are still side effects, e.g. loading file and storing, but let's leave it like that. Or, to make similarity between dependency injection and function composition even more striking, check out equivalent program in Haskell:

First of all IO monad is required to manage side effects. But do you see how importFile higher order function takes three parameters, but we can supply just two and get simpleImport? This is what we call dependency injection in Spring or EJB for that matter. But without syntax sugar.

Labels

Comments

Enlightening article, thanks. I also liked the pointer James Shore's article, which sums it up nicely.

I have one complaint: In transforming to the functional formulation, you silently changed the signature of Storage.save() from Payment -> UUID to Payment -> Void, or Consumer, in Java 8 terms. You hid that by a bit of hand-waving ("we just need a function that can store"), but in fact this wasn't a harmless reformulation. Things will get a bit murkier when trying to avoid that, I suppose?

One more remark, although it absolutely does not have to do with the point you are making, the Java example code has a little shortcoming that is often overlooked and might mislead people: File.lines(), according to the Java docs, should always be called with auto-close, like this:

However, the original method importFile might have been defined as returning the list of all the new primary keys.This would make the relation to functional composition even clearer, because we would actually be calling two functions, not just calling a terminal operation for its side effect only.Here's a possible Java 8 implementation:

As an afterthought, this example of returning something from a function that also does I/O inside, does make it more difficult to show the Haskell analog, no? Because we don't want the primary keys to stay inside the IO monad, I guess one would have to combine IO with the ST monad. Not really knowing Haskell, however, I wouldn't know how to do that exactly.

I don't know Haskell well either, but it's somewhat captured in the last example. storageFun has a type of Payment -> IO (), it should be IO UUID. The return type of simpleImport is wrong as well, it should be [IO UUID] or even better: IO [UUID]. But I don't think it breaks my train of thought and makes some of the code transformations lying (?).

There's actually one big upside of passing functions instead of whole objects. For example with `storage`, instead of depending on the whole collection of functions (`storage`), you depend only on the actual functionality that you are using. Testing is much easier, as well as figuring out what you actually do with the storage in a specific piece of functionality.

A downside: `Payment => Unit` type says much less than `Storage` type. You have to rely on parameter/variable names, which can easily get out-of-sync (they get duplicated in a lot of places, in fact each place in where they are used)

Some way of creating distinct types for specific functions would be nice (aliasing can make things easier, but aliases are as type-safe as a separate type) and useful here I suppose

Thanks for refreshing article! As a side note - since you touched Java (down to the bytecode), Scala and Haskell - have you looked at Frege language? They progressed recently a lot and may be reached some tipping point toward becoming relevant alternative as JVM language.

Initially I thought this was great. Now I wonder if the approach isn't sort of the wrong way around. Instead of passing functions ito the dependent object's methods, the dependent object should return functions that take the dependency as a parameter.

What makes the approach presented here seem plausible at first is that it is so simple, in that only one method methods of each dependency is used and these moreover are readily composable. But this will be rarely the case: usually, a class will call many different methods of the objects on which it depends. Furthermore, sometimes the injected class will offer many related methods with similar signatures, and the exact one to choose will have to take into account the exact manner of how the results are composed. Passing in all these functions amounts to a reductio-ad-absurdum of the idea. Instead, one must pass in the context type C (best an interface) , and return Functions from C to results. The "dependency injection" would then consist in applying this function to some instance of C outside the dependent class.

The context is the pair consisting of parser and storage instances, and importFile would return a function of such a pair. The difference may be minor, but it seems more intuitive to me. The downside is that you can't "do it just once".

Quote: "First of all I hope you are aware that object-oriented programming is a lie. Not because it's just a bunch of procedures in namespaces so-called classes (I hope you are not writing software this way). But because objects are implemented as procedures with implicit this parameter, when you see: this.database.insert(payment) it is actually compiled into something like this: Database.insert(this.database, payment)"

Quote: "First of all I hope you are aware that object-oriented programming is a lie. Not because it's just a bunch of procedures in namespaces so-called classes (I hope you are not writing software this way)"

Tomek, I totally agree with that opinion except the fact that we can use polymorphic behavior. If you are saying that object-oriented programming is not the best approach would do you recommend instead?