paint-brush
A functional approach to dependency injection in Javaby@johnmcclean
6,603 reads
6,603 reads

A functional approach to dependency injection in Java

by John McCleanAugust 11th, 2017
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Normally in <a href="https://hackernoon.com/tagged/java" target="_blank">Java</a> we use frameworks that resolve dependencies at runtime, and this can often result in hard to track down errors on deployment. A more functional alternative is to structure our programs slightly differently so that dependencies can be injected lazily in a type safe manner. The compiler and not runtime will identify and catch errors for us. Let’s start with a question:

Coin Mentioned

Mention Thumbnail
featured image - A functional approach to dependency injection in Java
John McClean HackerNoon profile picture

SuperKleisliIsFantasticFrameworksAreAtrocious

Normally in Java we use frameworks that resolve dependencies at runtime, and this can often result in hard to track down errors on deployment. A more functional alternative is to structure our programs slightly differently so that dependencies can be injected lazily in a type safe manner. The compiler and not runtime will identify and catch errors for us. Let’s start with a question:

How can we convert a method’s signature, so that input parameters are moved to the return type?

Given the Java method signature below, can we refactor the code such that the DAO becomes part of the return type and not a method parameter?

public String loadName(DAO dao, long id);

Imagine if our loadName method was implemented as follows



public String loadName(DAO dao, long id){return dao.loadRecord(id).getName();

}

One solution is to return a function.

If we were to return a Function rather than a String, we could make DAO an input to that Function and remove it as a method parameter.

public Function<DAO,String> loadName(long id);

Now we switch our eager implementation to a lazy one.




public Function<DAO,String> loadName(long id){return dao -> dao.loadRecord(id).getName();}

To actually load the users name, we need to pass in a DAO instance.

String name = loadName(10l).apply(dao);

Lazy dependency injection

In the example above we have removed the hard dependency on having a DAO when loadName is called. As we shall see in the rest of the article it is possible to chain function calls together, to work with the loaded name and only provide the dependency (DAO) at a point in the code where we have it to hand.

Just as we can lazily resolve dependencies injected in method calls, we can use Functions to delay the injection of Objects we may have stored as fields.

Converting the Object above to return a Function that injects the FieldType would look very similar to the method level refactoring performed previously.

Composing Functions

So far we have defined a way where by we can lazily inject a dependency one method call deep. When we call loadName we recieve a function, by executing the Function we can access the name.

Photo by Jakob Owens on Unsplash

It is possible, but a little more complex to work with the returned name without executing the Function. We can chain operations together on the returned Function.

To build a String that contains the users name with the eager version of this method we can write

public String loadName(DAO dao, long id);

String userData = "User's name is " + loadName(dao,10l);

But with the lazy version we will need to work with the Function’s andThen method

public Function<DAO,String> loadName(long id);

loadName(10l).andThen(name->"User's name is "+ name);

Function<DAO,String> sentence = loadName(10l).andThen(name->"User's name is "+ name);

System.out.println(sentence.apply(dao));

Where the output type of one Function matches the input type of another, we can chain them together using andThen.

Chaining method calls

andThen allows us to chain Function calls together, but to chain methods that return Functions we need something a little more powerful.


public Function<DAO,String> loadName(long id);public Function<DAO,Boolean> updateName(long id, String name);

Using andThen we would end up with a nested data structure (a Function that returns a Function).

Function<DAO,Function<DAO,Boolean>> fn = loadName(10).andThen(s->updateName(10,+” -suffix”));

To call our Function we’d need to apply our DAO twice

System.out.println(fn.apply(dao).apply(dao));

The Reader Monad: A useful pattern for nested data structures

Monads are a very useful pattern in functional programming for dealing with nested datastructures. This is something they excel at. E.g. We can merge calls to methods that return Optionals inside a chain of operations on Optional and still have a nice unnested single Optional as a result. The flatMap operator flattens the nested Optionals into a single Optional.

Although we could unwind the Function calls by hand so they are not nested, there is a Monad for Functions -the Reader Monad.

Function<DAO,Boolean> fn = dao->updateName(10,loadName(dao)+" -suffix"));

The Reader Monad allows us to use flatMap and map (just like for Streams and Optional) instead. To switch from using Function to Reader, just replace Function with Reader as the return type. (Reader is typically implemented as an extended Function)


public Reader<DAO,String> loadName(long id);public Reader<DAO,Boolean> updateName(long id, String name);

Reader<DAO,Boolean> r= loadName(10).flatMap(s->updateName(10,s));

When composing Functions directly output types should map to input types, when composing method calls using the Reader Monad it is more important that the input type to the returned Reader is the same. The output type is provided as a parameter during flatMap / map operation.

composition of returned Readers all accept DAO

For comprehensions

Once we start chaining a lot of method calls together nested flatMaps and maps can become a little unwieldy. Stucturing our calls using a for comprehension can help us keep our code a little cleaner. Let’s introduce a new method to log sucesses & failures

public boolean logIfFail(long id, String name, boolean success);

Chaining a call into this method results in more complex expression

Reader<DAO,Boolean> r= loadName(10).flatMap(s->updateName(10,s)

                        .map(success->logIfFail(10,s,success));

We can simplify things somewhat by using a for comprehension

We can see the real benefit of for comprehensions when introducing another method call further up the chain, without for comprehensions things can get really messy.

public Reader<DAO,Long> findNextId();

Our sequence of calls could become

Reader<DAO,Boolean> r= findNextId().flatMap(id->loadName(id).flatMap(s->updateName(10,s)

.map(success->logIfFail(10,s,success)));

Which looks a lot cleaner when expressed as a for comprehension

Awesome, so now we can delay the injection of key dependencies such as DAO’s, have our methods return Reader Monads that will lazily accept our DAO and chain them together using for comprehensions. To execute our chain of functions we can pass the DAO to our Reader and all of the methods we’ve chained together will execute.

boolean success = updateName.apply(dao);

Asynchronous Readers = complex type signatures!

But what happens when we have methods that return other monad types? Reading and writing to a remote store are IO bound operations, perhaps our methods should make use of Futures? Our type signatures are about to get messy!




public Reader<DAO,Future<Long>> findNextId();public Reader<DAO,Future<String>> loadName(long id);public Reader<DAO,Future<Boolean>> updateName(long id, String name);public Reader<Future<Boolean>> logIfFail(long id, String name, boolean success);

Even for the somewhat simpler use case of chaining together loadName, updateName and logIfFail, our for comprehension becomes horrendously complex!

Kleisli to the rescue

Kleisli is a function that returns a monadic type. It can represent generically a Function that returns a Future, Stream, Optional or any other monad type in Java. Much like Reader we can define map and flatMap methods for it too. The magic is that although the two types below are equivalent, it is much simpler to work with kleisli than with reader.


Reader<DAO,Future<Long>> reader;Kleisli<future,DAO,Long> kleisli;

Let’s rework our methods to return Kleisli instead




public Kleisli<future,DAO,Long> findNextId();public Kleisli<future,DAO,String> loadName(long id);public Kleisli<future,DAO,Boolean> updateName(long id, String name);public Future<Boolean> logIfFail(long id, String name, boolean success);

We can use flatMap and map to chain together method calls using Kleisli, but unlike in our previous examples with just the Reader monad, we now have lazy injection of dependencies and asynchronous execution.

Kleisli<future,DAO,Boolean> k= loadName(10).flatMapK(s->updateName(10,s));

When composing method calls that returned Reader instances we had to keep the input type (the injected dependency) the same. When composing method calls that return Kleisli instances we need to keep the monad type and in the input type the same.

Chaining load and save operations

In Debashish Gosh’s awesome Functional and Reactive Domain Modeling he shows how to chain asynchronous calls to credit and debit an account inside a single method in Scala using Kleisli. We can use the same technique ourselves in Java.

If we define asynchronous load and save methods in terms of Kleisli

A processing method can leverage these to load, transform and save data by flatMapping the Kleisli calls.

Calling process results in a Kleisli instance which tees up the loading, transforming and saving, but doesn’t execute it. We execute the method chain by calling apply (and passing in a DAO implementation), when we do so our code will execute asynchronously.

Much cleaner code

Armed with Kleisli we can go back and refactor our code that searches for the next user Id, modifies and updates that users name to use Kleisli for comprehensions (coming in cyclops-react 2.2.0)

Many supported Java Monad types

Compose All the Things

The cyclops integration modules provide higher kinded encodings for Vavr, Functional Java, RxJava, RxJava 2, Reactor , JDK /cyclops-react and even Guava types.

A small sample of the supported types are shown below

AnyM based equivalent

cyclops-react provides a powerful abstraction AnyM for working with common monad types, and provides KleisliM which is a Kleisli equivalent when working with monads via AnyM.

Type safe lazy dependency injection and async execution

Kleisli provides a very powerful mechanism for doing type safe dependency injection. Unlike traditional Java dependency injection techniques, we won’t get runtime errors due to unfulfilled dependency relationships (or circularly defined dependencies). What’s more we are able to chain together lazy asynchronous effects without relying on runtime AOP or proxy objects, many more classes of errors will be detected by the compiler rather than the runtime.