Skip to main content

Dependency injection: syntax sugar over function composition

Quoting Dependency Injection Demystified:

"Dependency Injection" is a 25-dollar term for a 5-cent concept.

*James Shore, 22 Mar, 2006

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:

@Component
class PaymentProcessor {

    private final Parser parser;
    private final Storage storage;

    @Autowired
    public PaymentProcessor(Parser parser, Storage storage) {
        this.parser = parser;
        this.storage = storage;
    }

    void importFile(Path statementFile) throws IOException {
            try(Stream lines = Files.lines(statementFile)) {
                lines
                        .map(parser::toPayment)
                        .forEach(storage::save);
            }
    }

}


@Component
class Parser {
    Payment toPayment(String line) {
        //om-nom-nom...
    }
}


@Component
class Storage {

    private final Database database;

    @Autowired
    public Storage(Database database) {
        this.database = database;
    }

    public UUID save(Payment payment) {
        return this.database.insert(payment);
    }
}


class Payment {
    //...
}
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?

$ javap -c Storage.class 
...
  public java.util.UUID save(com.nurkiewicz.di.Payment);
    Code:
       0: aload_0
       1: getfield      #2                  // Field database:Lcom/nurkiewicz/di/Database;
       4: aload_1
       5: invokevirtual #3                  // Method com/nurkiewicz/di/Database.insert:(Lcom/nurkiewicz/di/Payment;)Ljava/util/UUID;
       8: areturn
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 procedure Database.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.

Forget about objects

Let's make all of this more explicit and forget about objects:

class ImportDependencies {

    public final Parser parser;
    public final Storage storage;
    
    //...

}

static void importFile(ImportDependencies thiz, Path statementFile) throws IOException {
    Files.lines(statementFile)
            .map(thiz.parser::toPayment)
            .forEach(thiz.storage::save);
}
That's mad! Notice that importFile procedure 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:

static void importFile(Parser parser, Storage storage, Path statementFile) throws IOException {
    Files.lines(statementFile)
            .map(parser::toPayment)
            .forEach(storage::save);
}
Actually some people prefer passing dependencies explicitly to business methods like above, but that's not the point. It's just another step in the transformation.

Currying

For the next step we need to rewrite our function into Scala:

object PaymentProcessor {

  def importFile(parser: Parser, storage: Storage, statementFile: Path) {
    val source = scala.io.Source.fromFile(statementFile.toFile)
    try {
      source.getLines()
        .map(parser.toPayment)
        .foreach(storage.save)
    } finally {
      source.close()
    }
  }

}
It's functionally equivalent, so not much to say. Just notice how importFile() belongs to object, so it's somewhat similar to static methods on a singleton in Java. Next we'll group parameters:

def importFile(parser: Parser, storage: Storage)(statementFile: Path) { //...
This makes all the difference. Now you can either supply all dependencies all the time or better, do it just once:

val importFileFun: (Path) => Unit = importFile(parser, storage)

//...

importFileFun(Paths.get("/some/path"))
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:

def importFile(parserFun: String => Payment, storageFun: Payment => Unit)(statementFile: Path) {
  val source = scala.io.Source.fromFile(statementFile.toFile)
  try {
    source.getLines()
      .map(parserFun)
      .foreach(storageFun)
  } finally {
    source.close()
  }
}
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:

let parseFun :: String -> Payment
let storageFun :: Payment -> IO ()
let importFile :: (String -> Payment) -> (Payment -> IO ()) -> FilePath -> IO ()

let simpleImport = importFile parseFun storageFun
// :t simpleImport
// simpleImport :: FilePath -> IO ()
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.

PS: Webucator did a video based on this article. Thanks!

Comments

  1. 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?

    ReplyDelete
  2. 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:

    try (Stream<String> stream = Files.lines(statementFile)) {
      stream.map(parser::toPayment).forEach(storage::save);
    }

    ReplyDelete
    Replies
    1. Thanks, indeed I didn't close the Stream, somehow I remember about it in Scala version.

      Wrt. returning UUID - would it make any difference? I am ignoring that return value anyway.

      Delete
  3. Yes, of course you're right.

    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:

    BiFunction<Function<String, Payment>, Function<Payment,UUID>, FunctionX<Path, List<UUID>, IOException>> importFile =
      (parserFun, storageFun) -> statementFile -> {
        try (Stream<String> stream = Files.lines(statementFile)) {
          return stream.map(parserFun).map(storageFun).collect(toList());
        }
      };
      
    FunctionX<Path, List<UUID>, IOException> importFileFun = importFile.apply(parser::toPayment, storage::save);

    where we (a bit awkwardly) use a variant of java.util.function.Function in order to deal with the IOException:

    @FunctionalInterface
    interface FunctionX<T, U, X extends Throwable> {
      U apply(T t) throws X;
    }

    ReplyDelete
    Replies
    1. 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.

      Delete
    2. 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 (?).

      Neverthless, thanks for pointing out this inconsistency.

      Delete
  4. 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

    ReplyDelete
    Replies
    1. Thanks Adam for interesting comment. Payment => Unit (actually in Haskell it's slightly more descriptive Payment -> IO ()) in Java 8 can be represented as functional interface, e.g. PaymentStoreFun extends Consumer, which is more readable and type safe.

      Delete
  5. 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.

    ReplyDelete
    Replies
    1. Yup, Frege is definitely on my TO-DO list :-).

      Delete
  6. So glad someone thinks like this!!! I think the same but got shot all the time :)

    ReplyDelete
  7. 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.

    ReplyDelete
    Replies
    1. I'll try to make myself a little clearer.

      First, by "pass in the context type C" I just meant importing it, not passing in an instance.

      Second, my issue is only with your section "Functions all the way down". I think now that idea is not good, but I agree with everything up to that point.

      Lastly, I would prefer doing the application to the parser and storage object last, not first, so I would not write

      val importFileFun: (Path) => Unit = importFile(parser, storage)
      //...
      importFileFun(Paths.get("/some/path"))

      But something like

      importFile(Paths.get("/some/path")).apply(parser, storage)

      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".

      Delete
  8. 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)"

    Just FYI. Since Java 8, it's even possible syntactically to use _explicit_ this, the code below is perfectly valid Java code:
    package demo;
    public class Python {
    public void work(Python this, Object other) {

    }
    }

    ReplyDelete
    Replies
    1. 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?

      Delete
  9. I wish you happy NEW Year and ask you to check out this character analysis essay blog post

    ReplyDelete

Post a Comment