Since version 1.8, the OO-language Java provides functional facilities. In other words, you can pass methods around like objects now, as you always could in C or JavaScript, because these are not object-oriented languages.
Couldn't we do this with older Java as well?
What does this have to do with being object-oriented?
The Problem
Imagine you have following classes:
public class Greeting { private Object id; public Object getId() { return id; } public void setId(Object id) { this.id = id; } }
I call this "entity", because it can be mapped to a database table through an ORM (even if such a greeting doesn't make much sense:-).
public class Dao { public String create(Greeting entity) { return "Successfully created "+entity+"!"; } public Greeting find(Object identity) { Greeting greeting = new Greeting(); greeting.setId(identity); return greeting; } public String delete(Object identity) { return "Deleted "+identity; } }
Of course this is a mock Data-Access-Object (DAO), normally it would interact with a database.
public class Service { private final Dao dao = new Dao(); public String create(Greeting entity) { return dao.create(entity); } public Greeting read(Object identity) { return dao.find(identity); } public void delete(Object identity) { dao.delete(identity); } private static class Transaction { public void begin() { System.out.println("Start transaction"); } public void commit() { System.out.println("Commit transaction"); } public void rollback() { System.out.println("Rollback transaction"); } } }
This mock service exposes everything the DAO provides, and it contains a Transaction
class.
Now the challenge is to wrap a transaction around each DAO method call in the service, like this:
public String create(Greeting entity) { Transaction transaction = new Transaction(); try { transaction.begin(); String message = dao.create(entity); transaction.commit(); return message; } catch (Throwable th) { transaction.rollback(); throw th; } } public Greeting read(Object identity) { Transaction transaction = new Transaction(); try { transaction.begin(); Greeting entity = dao.find(identity); transaction.commit(); return entity; } catch (Throwable th) { transaction.rollback(); throw th; } } public void delete(Object identity) { Transaction transaction = new Transaction(); try { transaction.begin(); dao.delete(identity); transaction.commit(); } catch (Throwable th) { transaction.rollback(); throw th; } }
Like that source? Not a good solution, too much code duplication, this will cause maintenance problems when transaction handling needs to be changed, we can't repeat this for every DAO call.
With Java 8 we can do better. Here is the goal:
I want to call different methods with different parameters, but any such call needs to be accompanied by something that happens before and something that happens after, and this something is always the same for all the methods. Return objects from calls must not get lost.
Something like this:
public SomeResultType someMethod(SomeParameterType parameter) { Transaction transaction = new Transaction(); try { transaction.begin(); SomeResultType result = dao.someMethod(parameter); transaction.commit(); return result; } catch (Throwable th) { transaction.rollback(); throw th; } }
The road map:
- acquire a transaction
- call the method
- commit the transaction, or rollback on exception
- return whatever the method returned
This is something that an AOP "Around" advice could do.
The Solution
We can implement this as shown below.
Following source imports java.util.function.Function
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public String create(Greeting entity) { return transactional(e -> dao.create(e), entity); } public Greeting read(Object identity) { return transactional(id -> dao.find(id), identity); } public void delete(Object identity) { transactional(id -> dao.delete(id), identity); } private <P,R> R transactional(Function<P,R> daoFunction, P parameters) { final Transaction transaction = new Transaction(); try { transaction.begin(); final R result = daoFunction.apply(parameters); transaction.commit(); return result; } catch (Throwable th) { transaction.rollback(); throw th; } } |
How to read this?
Let's start on line 13 with the transactional()
method.
The method defines that it uses the generic types P and R. Better
would be to write this as <PARAMETER,RETURN>
.
Then it defines its return type as being the R of the preceding type definitions.
As parameters, transactional()
requires a Function
, this is a
functional interface
provided by the Java runtime library (source see below).
The function is defined to accept a parameter of type P, and to return a type R, just like its wrapper.
Second parameter of transactional()
is the parameter to be passed to the given function.
In case the method had several parameters, we would need to use a
parameter object pattern.
Inside the transactional()
method is what was outlined in the "road map" above:
get a transaction, call the method, terminate the transaction.
Let's go to line 17. Here the given daoFunction
is actually called.
Mind that the method's owning object is not present here.
So, how can a DAO method get called without having the owning DAO?
Let's go to line 2. The create()
method requires the
Greeting
entity as parameter.
It delegates to the transactional()
method
to wrap the used DAO call into a transaction.
For that, it creates a Function
object
(lambda, closure)
as first parameter:
// lambda e -> dao.create(e)
This also could be written as
// lambda (Greeting e) -> { return dao.create(e); }
which may be more readable:
e
is the parameter, the arrow points to the function body,
the last expression of the function body automatically is the return object.
Second parameter to transactional()
is the parameter that will be given to the lambda
when its apply()
method gets called, i.e. the entity that should be created
by the DAO.
Here is the essence of Java's functional interface for Function
:
@FunctionalInterface public interface Function<T, R> { R apply(T t); .... }
A functional interface must not contain more methods than just one,
but a Java 1.8 interface can also contain default
and static
implementations
(which was not possible in older Java).
In case a DAO method doesn't provide any return because it is void,
you could extend the lambda and return a dummy result object.
Or you could use Java's Consumer
as wrapper,
but then you would lose genericity between void and non-void methods.
Here is the essence of Consumer
:
@FunctionalInterface public interface Consumer<T> { void accept(T t); .... }
The only difference to Function
is the void return type,
which makes the generic type for the return-object dispensable.
The method name "accept" recalls the Visitee
role in the
visitor pattern,
but this is different.
Conclusion
Why hasn't this been available earlier in Java? It was, but we had to write a little more:
interface Function { Object execute(); } public String create(final Greeting entity) { return (String) transactional( new Function() { public Object execute() { return dao.create(entity); } }, entity ); }
Not as short and concise as lambdas.
The dangerous thing about passing functions around like objects is that they may lose
the this
pointer, their owning object, which is meant to be
their execution context.
Whoever used JavaScript may know about these subtle bugs when an event handler
establishes another this
than the developer intended.
Everything is context-bound.
But it is also about readability. Imagine a method that is passed as parameter to some other object, and from there again to some other object, what execution context would we expect for the method finally, as source code readers? The further away from its context, the less understandable the functionality.
Object-oriented means that methods and fields together form small units of functionality called classes, which can be the atoms of an application molecule. "Write less, do more" may sometimes reduce comprehensibility.
Keine Kommentare:
Kommentar veröffentlichen