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

Posted on by

Categories:     

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;

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;
}

and use it as follows-

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

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);

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);

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));

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()));

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);
}

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));

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;

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");
    }
}

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

         

Share on:

Author: A N M Bazlur Rahman

Java enthusiastic | Book author | Mentor | Helping Java Developers to improve their coding & collaboration skills so that they can meet great people & collaborate

100daysofcode 100daysofjava access advance-java agile algorithm arraylist article bangla-book becoming-expert biginteger book calculator checked checked-exceptions cloning code-readability code-review coding coding-convention collection-framework compact-strings completablefuture concatenation concurrency concurrentmodificationexception concurrentskiplistmap counting countingcollections critical-section daemon-thread data-race data-structure datetime day002 deliberate-practice deserialization design-pattern developers duration execute-around executors export fibonacci file file-copy fork/join-common-pool functional future-java-developers groupby hash-function hashmap history history-of-java how-java-performs-better how-java-works http-client image import inspiration io itext-pdf java java-10 java-11 java-17 java-8 java-9 java-developers java-performance java-programming java-thread java-thread-programming java11 java16 java8 lambda-expression learning learning-and-development linkedlist list local-type-inference localdatetime map methodology nio non-blockingio null-pointer-exception object-cloning optional packaging parallel pass-by-reference pass-by-value pdf performance prime-number programming project-loom race-condition readable-code record refactoring review scheduler scrum serialization serversocket simple-calculator socket software-development sorting source-code stack string string-pool stringbuilder swing thread threads tutorial unchecked vector virtual-thread volatile why-java zoneid