Venkat Subramaniam Brings a Contemporary Twist to GoF Design Patterns With Modern Java at Devoxx BE

MMS Founder
MMS Olimpiu Pop

Article originally posted on InfoQ. Visit InfoQ

The GoF Design Patterns published back in 1998, qualifies as a classic of computer science as it is still being taught both in universities but also recommended as best practice in the industry. In his deep dive session from Devoxx, Venkat Subramaniam gave them a contemporary twist, by implementing Iterator, Strategy, Decorator or Factory Method with modern Java.

In the introduction of his talk, Venkat considers the book authors to be grandfathers of software development and their design patterns with grandma’s recipes: even if you have them, you will not be able to reproduce the dish. So, he considers that design patterns are phenomenal as a communication tool, but a disaster as a software design tool.

The following are usual patterns that we could meet in our day-to-day coding, which he made a bit more fluent all in his energetic and joyful manner.

The iterator pattern changed quite a lot due to Java’s embrace of functional programming. One of the biggest changes was the shift from an external iterator to an internal iterator, which came with Java’s functional API. With this change, you can evolve from using the verbose imperative style iteration

int count = 0;
for(var name: names) {
   if(name.length() == 4) {
     System.out.println(name.toUpperCase());
	 count++;

     if(count == 2) {
        break;
     }
   }
  }
}

to the fluent functional iteration:

names.stream()
     .filter(name -> name.length() == 4)
     .map(String::toString)
     .limit(2)
     .forEach(System.out::println);

The limit(long) and takeWhile(Predicate<? super T>) (added in Java 9) are the functional equivalents of continue and break statements, the first one taking just a numerical limit, while the second using an expression.

Even if Java’s functional API is part of the JDK for almost a decade already, there are still common mistakes that linger in the code bases. The one that can make the results of iteration operations unpredictable(especially in parallel executions) is when the functional pipeline is *not* pure (it changes or depends on any state visible from the outside).

Lightweight strategy – where we want to vary a small part of an algorithm while keeping the rest of the algorithm the same. Historically, the pattern was implemented by having a method that takes a single method interface as a parameter that has multiple implementations for each of the strategies to be implemented. As strategies are often a single method or function. So, functional interfaces and lambdas work really well. 

Even though anonymous classes represented an implementation mechanism, functional interfaces(Predicate<? super T> is a good candidate) or lambdas make the code a lot more fluent and easier to comprehend. In modern Java, Strategy is more of a feature than a pattern that requires significant effort to implement.

public class Sample {
  public static int totalValues(List numbers) {
    int total = 0;

    for(var number: numbers) {
      total += number;
    }

    return  total;
  }

  public static int totalEvenValues(List numbers) {
    int total = 0;

    for(var number: numbers) {
      if(number % 2 == 0) { total += number; }
    }

    return  total;
  }

  public static int totalOddValues(List numbers) {
    int total = 0;

    for(var number: numbers) {
      if(number % 2 != 0) { total += number; }
    }

    return  total;
  }


  public static void main(String[] args) {
    var numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

    System.out.println(totalValues(numbers));
    System.out.println(totalEvenValues(numbers));
    System.out.println(totalOddValues(numbers));
  }
}

The more modern take would be to use a lambda for the strategy: 

import java.util.function.Predicate;

public class Sample {
  public static int totalValues(List numbers,
    Predicate selector) {
    int total = 0;

    for(var number: numbers) {
      if(selector.test(number)) {
        total += number;
      }
    }

    return  total;
  }

  public static boolean isOdd(int number) {
    return number % 2 != 0;
  }

  public static void main(String[] args) {
    var numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

    System.out.println(totalValues(numbers, ignore -> true));
    System.out.println(totalValues(numbers, 
      number -> number % 2 == 0));

    System.out.println(totalValues(numbers, Sample::isOdd));
  }
}

Factory method using default methods

In the introduction to the factory method implementation, Venkat stated the following:

What is the worst keyword in Java from the polymorphism point of view? […] Even though final, instanceof and static are good candidates for this, they are mininions. new is the mafia of all of them.

Multiple patterns (creational patterns), frameworks(Spring, Guice) or techniques were conceived in order to address the “evilness” of new, its lack of polymorphism support and its tight coupling. Inspired by Ruby’s polymorphic ability to create different objects based on context, Venkat proposes an implementation of the factory method pattern by using Java’s default keyword. This approach would allow one to make use of interfaces and very small implementing classes, making the code easier to follow.

import java.util.*;

interface Pet {}
class Dog implements Pet {}
class Cat implements Pet {}

interface Person {
  Pet getPet();

  default void play() {
    System.out.println("playing with " + getPet());
  }
}

class DogPerson implements Person {
  private Dog dog = new Dog();

  public Pet getPet() { return dog; }
}

class CatLover implements Person {
  private Cat cat = new Cat();
  public Pet getPet() { return cat; }
}

public class Sample {
  public static void call(Person person) {
    person.play();
  }

  public static void main(String[] args) {
    call(new DogPerson());
    call(new CatLover());
  }
}

Decorator

Even if the decorator pattern is theoretically well-known by many programmers, few actually implemented it in practice. Probably the most infamous example of its implementation is the construction of io packages. Venkat proposes a different approach to this pattern, based on the functions composability: by using the identity function and andThen(Function ) he has the ability to build simple, fluent mechanisms that enhance the abilities of an object.

class Camera {
  private Function filter;

  public Camera(Function... filters) {
    filter = Stream.of(filters)
      .reduce(Function.identity(), Function::andThen);
  }

  public Color snap(Color input) {
    return filter.apply(input);
  }
}

public class Sample {
  public static void print(Camera camera) {
    System.out.println(camera.snap(new Color(125, 125, 125)));
  }

  public static void main(String[] args) {
    print(new Camera());

    print(new Camera(Color::brighter));
    print(new Camera(Color::darker));

    print(new Camera(Color::brighter, Color::darker));
  }
}

Even if the patterns seem to be forever young, as Venkat Subramaniam mentioned during his talk: “Design patterns often kick in to fill the gaps of a programming language. The more powerful a language is, the less we talk about design patterns as these naturally become the features of the language.”

Together with the evolution of programming languages and of our experience evolve also the patterns as time goes on. Some of them are absorbed as features of the languages, others were deemed obsolete and others are easier to implement. Regardless of which category your favourite falls into, Venkat suggests using them as communication means and letting the code evolve towards them. Also, he recommends experimenting with multiple programming languages, as a way to make your coding more fluent.

About the Author

Subscribe for MMS Newsletter

By signing up, you will receive updates about our latest information.

  • This field is for validation purposes and should be left unchanged.