DEV Community

A N M Bazlur Rahman
A N M Bazlur Rahman

Posted on • Originally published at bazlur.com on

Let's create coffee with a decorator pattern with the help of lambda expression

java.util.function.Function<T, R> is a functional interface. It takes a type T and returns type R. We mostly use it to transform a type into a different one. But it doesn’t end there. We can use this into a lambda expression to change a value as well. For example-

Function<Integer, Integer> doubleIt = a -> a * 2;

Enter fullscreen mode Exit fullscreen mode

Over here, we are receiving and in integer but returning after doubling it. Thus, we can pass this function as an argument of a method as well.

static Integer transform(Integer value, Function<Integer, Integer> func) {
    var applied = func.apply(value);
    return applied;
}

Enter fullscreen mode Exit fullscreen mode

and use it as follows-

var doubled = transform(4, doubleIt);
System.out.println("doubled = " + doubled);

Enter fullscreen mode Exit fullscreen mode

It will print 8.

Similarly, we can pass many other lambda expressions into the transform method.

Function<Integer, Integer> squareIt = a -> a * a;
Function<Integer, Integer> cubeIt = a -> a * a * a;

var squared = transform(4, squareIt);
System.out.println("square of 4 = " + squared);
var cubed = transform(5, cubeIt);
System.out.println("cube of 5 = " + cubed);

Enter fullscreen mode Exit fullscreen mode

We may have another requirement, for example, we need to cube a value and then increment it by 1. We can do it in two steps.

Function<Integer, Integer> cubeIt = a -> a * a * a;
Function<Integer, Integer> incrementByOne = a -> a + 1;

var cubedOf5 = cubeIt.apply(5);
var cubedAndIncremented = incrementByOne.apply(cubedOf5);

Enter fullscreen mode Exit fullscreen mode

However, the above code doesn’t look concise at all. We can fix it using the default method of Function interface, which is andThen(), and it takes a function as an argument. Using it, we can chain up multiple functions together. The above code would look like this -

transform(5, cubeIt.andThen(incrementByOne));

Enter fullscreen mode Exit fullscreen mode

This is a one-liner and looks concise and straightforward.

We can use this concept to implement the decorator pattern. This pattern lets you attach new behaviours to objects at runtime.

We will make coffee with as many ingredients as we can put into a cup using the decorator pattern. When we’d do it, our coffee cup would like this-

var ingredients = List.of("Tim Horton");
var coffeeCup = new CoffeeCup(ingredients);

var coffee = getCoffeeWithExtra(coffeeCup,
            Coffee::withDarkCookieCrumb,
            Coffee::withSaltedCaramelFudge,
            Coffee::withSweetenedMilk,
            Coffee::withVanillaAlmondExtract);

System.out.println("Coffee with " + String.join(", ", coffee.ingredients()));

Enter fullscreen mode Exit fullscreen mode

We will grab a Tim Horton and then keep adding extra ingredients to it using the getCoffeeWithExtra() method.

Let’s look at the getCoffeeWithExtra() method.

static Coffee getCoffeeWithExtra(Coffee coffee, Function<Coffee, Coffee>... ingredients) {
    var reduced = Stream.of(ingredients)
                .reduce(Function.identity(), Function::andThen);
    return reduced.apply(coffee);
}

Enter fullscreen mode Exit fullscreen mode

This method takes a Coffee and an array of functions that transform the coffee, exactly the way we saw in the transform method. We have reduced the function array into one using stream and then applied it to the coffee and returned it. Let’s look into the Coffee interface. It has only one method and a few static methods.

The stream has this reduce function, which takes an identity function as its first parameter; the second parameter is another functional interface named BinaryOperator. The identity function is a function that doesn’t do anything; basically, it returns the same value that it gets as a parameter. Without the method reference, it looks like the following-

    var reduce1 = Stream.of(ingredients)
            .reduce(kopi -> kopi, (func1, func2) -> func1.andThen(func2));

Enter fullscreen mode Exit fullscreen mode

Note(From javaDoc): The reduce function performs a reduction on the elements of this stream, using the provided identity value and an associative accumulation function, and returns the reduced value. This is equivalent to:

 T result = identity;
 for (T element : this stream)
     result = accumulator.apply(result, element)
 return result;

Enter fullscreen mode Exit fullscreen mode

Let’s look at the Coffee interface now.


 @FunctionalInterface 
 interface Coffee {
    List<String> ingredients();  

    static Coffee withSaltedCaramelFudge(Coffee coffee) {
            return () -> coffee.add("Salted Caramel Fudge");
    }

    default List<String> add(String item) {
            return new ArrayList<>(ingredients()) {{
                    add(item);
            }};
    }

    static Coffee withSweetenedMilk(Coffee coffee) {
            return () -> coffee.add("Sweetened Milk");
    }

    static Coffee withDarkCookieCrumb(Coffee coffee) {
            return () -> coffee.add("Dark Cookie Crumb");
    }

    static Coffee withVanillaAlmondExtract(Coffee coffee) {
            return () -> coffee.add("Vanilla/Almond Extract");
    }
}

Enter fullscreen mode Exit fullscreen mode

These static methods are for convenience. We can keep adding these methods as our ingredient list grows. The benefit is, when we will use them, we will be able to use method references, which will make our code concise.

Let’s put everything together.

These static methods are for convenience. We can keep adding these methods as our ingredient list grows. The benefit is, when we will use them, we will be able to use method references, which will make our code concise.

Let’s put everything together.

package com.bazlur;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;

public class Day012 {

  public static void main(String[] args) {
    var ingredients = List.of("Tim Horton");
    var coffeeCup = new CoffeeCup(ingredients);

    var coffee = getCoffeeWithExtra(coffeeCup,
            Coffee::withDarkCookieCrumb,
            Coffee::withSaltedCaramelFudge,
            Coffee::withSweetenedMilk,
            Coffee::withVanillaAlmondExtract);

    System.out.println("Coffee with " + String.join(", ", coffee.ingredients()));
  }

  @SafeVarargs
  static Coffee getCoffeeWithExtra(Coffee coffee, Function<Coffee, Coffee>... ingredients) {
    var reduced = Stream.of(ingredients)
            .reduce(Function.identity(), Function::andThen);
    return reduced.apply(coffee);
  }

  @FunctionalInterface
  interface Coffee {
    static Coffee withSaltedCaramelFudge(Coffee coffee) {
      return () -> coffee.add("Salted Caramel Fudge");
    }

    default List<String> add(String item) {
      return new ArrayList<>(ingredients()) {{
        add(item);
      }};
    }

    List<String> ingredients();

    static Coffee withSweetenedMilk(Coffee coffee) {
      return () -> coffee.add("Sweetened Milk");
    }

    static Coffee withDarkCookieCrumb(Coffee coffee) {
      return () -> coffee.add("Dark Cookie Crumb");
    }

    static Coffee withVanillaAlmondExtract(Coffee coffee) {
      return () -> coffee.add("Vanilla/Almond Extract");
    }
  }

  record CoffeeCup(List<String> initialIngredient) implements Coffee {
    @Override
    public List<String> ingredients() {
      return initialIngredient;
    }
  }
}


% java Day012.java 
Coffee with Tim Horton, Dark Cookie Crumb, Salted Caramel Fudge, Sweetened Milk, Vanilla/Almond Extract

Enter fullscreen mode Exit fullscreen mode

Top comments (0)