174. Implementing the Decorator pattern

The Decorator pattern prefers composition over inheritance; therefore, it is an elegant alternative to the subclassing technique. With this, we mainly start from a base object and add additional features in a dynamic fashion.

For example, we can use this pattern to decorate a cake. The decoration process doesn't change the cake itself –  it just adds some nuts, cream, fruit, and so on.

The following diagram illustrates what we will implement:

First, we create an interface called Cake:

public interface Cake {
String decorate();
}

Then, we implement this interface via BaseCake:

public class BaseCake implements Cake {

@Override
public String decorate() {
return "Base cake ";
}
}

Afterward, we create an abstract CakeDecorator class for this Cake. The main goal of this class is to call the decorate() method of the given Cake:

public class CakeDecorator implements Cake {

private final Cake cake;

public CakeDecorator(Cake cake) {
this.cake = cake;
}

@Override
public String decorate() {
return cake.decorate();
}
}

Next, we focus on writing our decorators.

Each decorator extends CakeDecorator and alters the decorate() method to add the corresponding decoration.

For example, the Nuts decorator looks like this:

public class Nuts extends CakeDecorator {

public Nuts(Cake cake) {
super(cake);
}

@Override
public String decorate() {
return super.decorate() + decorateWithNuts();
}

private String decorateWithNuts() {
return "with Nuts ";
}
}

For brevity purposes, we skip the Cream decorator. However, it is pretty straightforward to intuit that this decorator is mostly the same as Nuts.

So, again, we have some boilerplate code.

Now, we can create a Cake decorated with nuts and cream, as follows:

Cake cake = new Nuts(new Cream(new BaseCake()));
// Base cake with Cream with Nuts

System.out.println(cake.decorate());

So, this is a classical implementation of the Decorator pattern. Now, let's take a look at the lambda-based implementation, which drastically reduces this code. This is especially the case when we have a significant number of decorators.

This time, we transform the Cake interface into a class, as follows:

public class Cake {

private final String decorations;

public Cake(String decorations) {
this.decorations = decorations;
}

public Cake decorate(String decoration) {
return new Cake(getDecorations() + decoration);
}

public String getDecorations() {
return decorations;
}
}

The climax here is the decorate() method. Mainly, this method applies the given decoration next to the existing decorations and returns a new Cake.

As another example, let's consider the java.awt.Color class, which has a method named brighter(). This method creates a new Color that is a brighter version of the current Color. Similarly, the decorate() method creates a new Cake that is a more decorated version of the current Cake.

Furthermore, there is no need to write decorators as separate classes. We will rely on lambdas to pass the decorators to the CakeDecorator:

public class CakeDecorator {

private Function<Cake, Cake> decorator;

public CakeDecorator(Function<Cake, Cake>... decorations) {
reduceDecorations(decorations);
}

public Cake decorate(Cake cake) {
return decorator.apply(cake);
}

private void reduceDecorations(
Function<Cake, Cake>... decorations) {

decorator = Stream.of(decorations)
.reduce(Function.identity(), Function::andThen);
}
}

Mainly, this class accomplishes two things:

  • In the constructor, it calls the reduceDecorations() method. This method will chain the array of the passed Function via the Stream.reduce() and Function.andThen() methods. The result is a single Function composed from the array of the given Function.
  • When the apply() method of the composed Function is called from the decorate() method, it will apply the chain of given functions one by one. Since each Function in the given array is a decorator, the composed Function will apply each decorator one by one.

Let's create a Cake decorated with nuts and cream:

CakeDecorator nutsAndCream = new CakeDecorator(
(Cake c) -> c.decorate(" with Nuts"),
(Cake c) -> c.decorate(" with Cream"));

Cake cake = nutsAndCream.decorate(new Cake("Base cake"));

// Base cake with Nuts with Cream
System.out.println(cake.getDecorations());

Done! Consider running the code bundled with this book to check the output.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.119.106