Java 8 Lambda expressions or how to write a better quality code

Lambda that relies mainly on the Java 8 functional interface concept has two faces :

– it is really necessary to use streams that were also introduced in Java 8

– beyond this use with streams, it is also a excellent language feature to kill verbosity, boiler plate code and some dumb code duplication that were unfortunately required in some cases before Java 8.

We will here discuss of the second point.

Example of a class with verbose and duplicated code.

Suppose we declare a MailService class that exposes operations to send, resend and cancel a message.
These operations follow a common logic :
– checking that the connection is established and establish it if required
– doing the task
– historying the passed operation

Finally, most of tasks performed by an operation are common.

Here is a first implementation of the class :

package davidhxxx.basics.java8.lambda;
 
import java.time.LocalDateTime;
 
public class MessageService {
 
    private Transport transport;
    private History history;
 
    public MessageService(Transport transport, History history) {
	this.transport = transport;
	this.history = history;
    }
 
    public void sendMessage(String message) {
	connectIfRequired();
	transport.sendMessage(message);
	historizePassedCommand("sendMessage with message =" + message);
    }
 
    public void replaceLastSentMessage(String message) {
	connectIfRequired();
	transport.replaceLastMessage(message);
	historizePassedCommand("replaceLastMessage with message =" + message);
    }
 
    public void cancelLastMessage() {
	connectIfRequired();
	transport.cancelLastSentMessage();
	historizePassedCommand("cancelLastSentMessage");
    }
 
    private void connectIfRequired() {
	if (transport.ping() == null) {
	    transport.connect();
	}
    }
 
    private void historizePassedCommand(String command) {
	history.add(command, LocalDateTime.now());
    }
 
}

The code reduces the duplication by extracting the connection and the history tasks in two distinct methods.
In this way, each operation may call it.
It is not bad but this has drawbacks.  It is verbose and we are constraint to read the content of each operation to understand that pre and post tasks are the same for all operations. If all operation methods relied on a single method that handles the whole flow, the reading of this single method would be enough to understand the overall logic of operations.
Besides, it is not very maintainable. At each time a new operation is added, we must remember to call the pre and post task.
If we forget it, the processing may fail at runtime (if the connection is not established) or we could lose some history information.
Suppose a new post-task is added. For example, triggering automatic rules applicable after the operation. To achieve it, we have to modify all operations to add this task.

Introducing an interface with a single method to mime the method passing as argument

Ideally, we would not repeat the pre and post operation task invocation (connection and history) in each operation method.
We would have a common method that is able to perform any operation and its pre and post task.
To do it, we would like to pass the operation method to invoke to the common method.
But before Java 8, there is not a real straight way to do it.
A workaround to do it is introducing a specific interface that declares a single method. The method mimes the method passing or the lambda expressions that are not provided before Java 8.
The method signature depends on the needs.
For example, if we want to do a processing according to a boolean condition, the  interface method returns a boolean value. 
Other example, if we want to construct an object from a String, the interface method takes as argument a String and returns the class of the object to construct.
And so for…
The general idea is that we have a way to parameterize a method as if we could pass a method as argument to this method.
To come back to your need, we could have a MailTask interface that declares a void invoke(String message) method :

public interface MailTask {   
    void invoke(String message);
}

Now, it becomes possible to introduce a common method to all operations that perform the general flow of them :

private void commonOperation(String message, String command, MailTask mailTask) {
   connectIfRequired();
   mailTask.invoke(message);
   historizePassedCommand(command);
}

Then, each operation could define its own implementation of the interface.
Of course, defining the implementation in a named class is really not required as the class is not reused (in our use case) and so it makes the code harder to read without valuable motivations.
We would privilege the use of anonymous class in each operation method.

So, we could get this new version of MessageService:

package davidhxxx.basics.java8.lambda;
 
import java.time.LocalDateTime;
 
public class MessageServiceImprovementWithoutLambda {
 
    private Transport transport;
    private History history;
 
    public MessageServiceImprovementWithoutLambda(Transport transport, History history) {
	this.transport = transport;
	this.history = history;
    }
 
    public void sendMessage(String message) {
	// FIXME no way to pass as an argument the expression "transport.sendMessage(message);"
	// so we create an anonymous class to handle it
 
	commonOperation(message, "sendMessage with message =" + message, new MailTask() {
 
	    @Override
	    public void invoke(String message) {
		transport.sendMessage(message);
	    }
 
	});
    }
 
    public void replaceLastSentMessage(String message) {
	//FIXME Same problem
	commonOperation(message, "replace with message =" + message, new MailTask() {
 
	    @Override
	    public void invoke(String message) {
		transport.replaceLastMessage(message);
	    }
 
	});
    }
 
    public void cancelLastSentMessage(String message) {
	//FIXME Same problem
	commonOperation(message, "cancelLastSentMessage", new MailTask() {
 
	    @Override
	    public void invoke(String message) {
		transport.cancelLastSentMessage();
	    }
 
	});
    }
 
    private void commonOperation(String message, String command, MailTask mailTask) {
	connectIfRequired();
	mailTask.invoke(message);
	historizePassedCommand(command);
    }
 
    private void connectIfRequired() {
	if (transport.ping() == null) {
	    transport.connect();
	}
    }
 
    private void historizePassedCommand(String command) {
 	history.add(command, LocalDateTime.now());
    }
 
}

This solution may seem better that the previous one as it avoids method invocation duplication and so makes the maintainability better to add or remove pre and post tasks and operations but it has still drawbacks.
It is still more verbose that the previous one and the use of anonymous classes to pass them as a parameter reduces also the readability of the operation methods.
So finally it both increases and decreases the maintainability of the class.
The other problem brought by this solution is the introduction of a custom interface that doesn’t make part of the JDK.
In each project, module we are developing, we will have to create our own custom interfaces or import them as a library but it is not necessary a common practice shared by all team members. If interfaces for simple and common need was provided by the JDK, it would be much better as it could become de facto a standard practice.

Using standard Functional interfaces of Java 8 mixed with lambda 

The previous solution has three main drawbacks:
– use of a custom interface
– verbosity
– low maintainability

Java 8 and Lambdas expressions address all of them.
Java 8 introduces the notion of functional interface : a interface with a single non default method.
It also provides multiple functional interfaces to address common needs.
For example, in your case, we could replace the MailTask interface by the Consumer interface that does the job and more :

package java.util.function;
 
@FunctionalInterface
public interface Consumer<T> {
 
    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
 
    /**
     * Returns a composed {@code Consumer} that performs, in sequence, this
     * operation followed by the {@code after} operation. If performing either
     * operation throws an exception, it is relayed to the caller of the
     * composed operation.  If performing this operation throws an exception,
     * the {@code after} operation will not be performed.
     *
     * @param after the operation to perform after this operation
     * @return a composed {@code Consumer} that performs in sequence this
     * operation followed by the {@code after} operation
     * @throws NullPointerException if {@code after} is null
     */
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

We could now update the commonOpertion() method by using the standard java.util.function.Consumer interface as parameter :

private void commonOperation(String message, Consumer<String> messageConsumer, String command) {
   connectIfRequired();
   messageConsumer.accept(message);
   historizePassedCommand(command);
}

The verbosity that decreases the maintainability in the previous version of the class is tackled by the lambda expressions. For example below we call commonOperation() with as second parameter a lambda expression matching to the Consumer functional interface descriptor : one String argument and no return.
In the body of the lambda, we invoke transport.sendMessage(m) that is a much straighter way to pass a method to execute :

public void sendMessage(String message) {
   commonOperation(message, (m) -> transport.sendMessage(m),"sendMessage with message =" + message );
}

We could even do shorter than  the lambda expression by using a method reference on the transport variable:

commonOperation(message,  transport::sendMessage,"sendMessage with message =" + message );

Here is the whole class with the lambda use :

package davidhxxx.basics.java8.lambda;
 
import java.util.function.Consumer;
 
public class MessageServiceWithLambda {
 
    private Transport transport;
    private History history;
 
    public MessageServiceWithLambda(Transport transport) {
	this.transport = transport;
    }
 
    public void sendMessage(String message) {
	commonOperation(message, (m) -> transport.sendMessage(m),"sendMessage with message =" + message );
    }
 
    public void replaceLastSentMessage(String message) {
	commonOperation(message, (m) -> transport.replaceLastMessage(m), "replaceLastMessage with message =" + message);
    }
 
    public void cancelLastSentMessage() {
	commonOperation(null, (m) -> transport.cancelLastSentMessage(), "cancelLastSentMessage");
    }
 
    private void commonOperation(String message, Consumer<String> messageConsumer, String command) {
	connectIfRequired();
	messageConsumer.accept(message);
	historizePassedCommand(command);
    }
 
    private void connectIfRequired() {
	if (transport.ping() == null) {
	    transport.connect();
	}
    }
 
    private void historizePassedCommand(String command) {
 	history.add(command, LocalDateTime.now());
    }
}

This solution is really nice : no duplication, straight readable, concise and maintainable. The historizePassedCommand() and connectIfRequired() that were introduced in the first implementation to avoid duplicate them is now even not required.
Their code is really simple. We could break the indirection to them by inlining them in commonOperation(). Of course, if their implementations were more complex and had more statements, it would make sense to keep them in distinct methods to separate clearly responsibilities.
Here is the new version of the class :

package davidhxxx.basics.java8.lambda;
 
import java.time.LocalDateTime;
import java.util.function.Consumer;
 
public class MessageServiceWithLambdaStillImproved {
 
    private Transport transport;
    private History history;
 
    public MessageServiceWithLambdaStillImproved(Transport transport, History history) {
	this.transport = transport;
	this.history = history;
    }
 
    public void sendMessage(String message) {
	commonOperation(message, (m) -> transport.sendMessage(m), "sendMessage with message =" + message);
    }
 
    public void replaceLastSentMessage(String message) {
	commonOperation(message, (m) -> transport.replaceLastMessage(m), "replaceLastMessage with message =" + message);
    }
 
    public void cancelLastSentMessage() {
	commonOperation(null, (m) -> transport.cancelLastSentMessage(), "cancelLastSentMessage");
    }
 
    private void commonOperation(String message, Consumer<String> messageConsumer, String command) {
	if (transport.ping() == null) {
	    transport.connect();
	}
	messageConsumer.accept(message);
	history.add(command, LocalDateTime.now());
    }
 
}
Ce contenu a été publié dans anonymous class, java, java 8, java8, lambda. Vous pouvez le mettre en favoris avec ce permalien.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *