With the advent of lambdas in Java we now have a new tool to better design our code. Of course the first step is using streams, method references and other neat features introduced in Java 8.
Going forward I think the next step is to revisit the well established Design Patterns and see them through the functional programming lenses. For this purpose I’ll take the Decorator Pattern and implement it using lambdas.
We’ll take an easy and delicious example of the Decorator Pattern: adding toppings to pizza. Here is the standard implementation as suggested by GoF:
First we have the interface that defines our component:
public interface Pizza {
String bakePizza();
}
We have a concrete component:
public class BasicPizza implements Pizza {
@Override
public String bakePizza() {
return "Basic Pizza";
}
}
We decide that we have to decorate our component in different ways. We go with Decorator Pattern. This is the abstract decorator:
public abstract class PizzaDecorator implements Pizza {
private final Pizza pizza;
protected PizzaDecorator(Pizza pizza) {
this.pizza = pizza;
}
@Override
public String bakePizza() {
return pizza.bakePizza();
}
}
we provide some concrete decorators for the component:
public class ChickenTikkaPizza extends PizzaDecorator {
protected ChickenTikkaPizza(Pizza pizza) {
super(pizza);
}
@Override
public String bakePizza() {
return super.bakePizza() + " with chicken topping";
}
}
public class ProsciuttoPizza extends PizzaDecorator {
protected ProsciuttoPizza(Pizza pizza) {
super(pizza);
}
@Override
public String bakePizza() {
return super.bakePizza() + " with prosciutto";
}
}
and this is the way to use the new structure:
Pizza pizza = new ChickenTikkaPizza(new BasicPizza());
String finishedPizza = pizza.bakePizza(); //Basic Pizza with chicken topping
pizza = new ChickenTikkaPizza(new ProsciuttoPizza(new BasicPizza()));
finishedPizza = pizza.bakePizza(); //Basic Pizza with prosciutto with chicken topping
we can see that this can get very messy, and it did get very messy if we think about how we handle buffered readers in java:
new DataInputStream(new BufferedInputStream(new FileInputStream(new File("myfile.txt"))))
of course, you can split that in multiple lines, but that won’t solve the messiness, it will just spread it.
Now lets see how we can do the same thing using lambdas.
We start with the same basic component objects:
public interface Pizza {
String bakePizza();
}
public class BasicPizza implements Pizza {
@Override
public String bakePizza() {
return "Basic Pizza";
}
}
But now instead of declaring an abstract class that will provide the template for decorations, we will create the decorator that asks the user for functions that will decorate the component.
public class PizzaDecorator {
private final Function<Pizza, Pizza> toppings;
private PizzaDecorator(Function<Pizza, Pizza>... desiredToppings) {
this.toppings = Stream.of(desiredToppings)
.reduce(Function.identity(), Function::andThen);
}
public static String bakePizza(Pizza pizza, Function<Pizza, Pizza>... desiredToppings) {
return new PizzaDecorator(desiredToppings).bakePizza(pizza);
}
private String bakePizza(Pizza pizza) {
return this.toppings.apply(pizza).bakePizza();
}
}
There is this line that constructs the chain of decorations to be applied:
Stream.of(desiredToppings).reduce(identity(), Function::andThen);
This line of code will take your decorations (which are of Function type) and chain them using andThen. This is the same as
(currentToppings, nextTopping) -> currentToppings.andThen(nextTopping)
and it sure that the functions are called subsequently in the order you provided.
Also Function.identity() is translated to elem -> elem lambda expression.
Ok, now where we’ll we define our decorations? You can add them as static methods in PizzaDecorator or even in the interface:
public interface Pizza {
String bakePizza();
static Pizza withChickenTikka(Pizza pizza) {
return new Pizza() {
@Override
public String bakePizza() {
return pizza.bakePizza() + " with chicken";
}
};
}
static Pizza withProsciutto(Pizza pizza) {
return new Pizza() {
@Override
public String bakePizza() {
return pizza.bakePizza() + " with prosciutto";
}
};
}
}
And now, this is how this pattern gets to be used:
String finishedPizza = PizzaDecorator.bakePizza(new BasicPizza(),Pizza::withChickenTikka, Pizza::withProsciutto);
//And if you static import PizzaDecorator.bakePizza:
String finishedPizza = bakePizza(new BasicPizza(),Pizza::withChickenTikka, Pizza::withProsciutto);
As you can see, the code got more clear and more concise, and we didn’t use inheritance to build our decorators.
This is just one of the many design pattern that can be improved using lambdas. There are more features that can be used to improve the rest of them like using partial application (currying) to implement Adapter Pattern.
I hope I got you thinking about adopting a more functional programming approach to your development style.
UPDATE: Here you can find a video walkthrough of this article, created by my friends from Webucator:
If you want to see more of their tutorials you can visit Webucator site
Bibliography:
The decorator example was inspired by Gang of Four – Decorate with Decorator Design Pattern article
The refactoring method was inspired by the following Devoxx 2015 talks (which I recommend watching as they treat the subject at large):
Design Pattern Reloaded by Remi Forax
Design Patterns in the Light of Lambda Expressions by Venkat Subramaniam
Mark January 4, 2016
Your static Pizza method in interface:
static Pizza withChickenTikka(Pizza pizza) {
return new Pizza() {
@Override
public String bakePizza() {
return pizza.bakePizza() + ” with chicken”;
}
};
}
can be even written shorter like this:
static Pizza withChickenTikka(Pizza pizza) {
return ( () -> pizza.bakePizza() + ” with chicken” );
}
Stefan Bulzan January 4, 2016 — Post Author
Yes, that’s true. But it’s based on the context that my Pizza interface has only one abstract method, which is exactly what how a FunctionalInterface should be. But usually this is not the case for objects to be decorated as most of objects cannot be expressed as a lambda. That’s why I decided not to use lambdas for this.