Design Patterns in Modern Java

Advertisements

Design patterns are solutions to general problems that software developers face during software development. Usually some of them are taken as best practices under specific situations because they have demonstrated that provides an efficient solution. However, the programming languages evolve with the time, adding new features, and new frameworks are created which provides new ways to develop, therefore the design patterns also evolve.

Let’s take as an example the Singleton, a design pattern that allows to have only one instance of a class across the application. It might considered an Anti-Pattern by some people, and in practice, using framework like Jakarta EE, Quarkus, Spring, or Micronaut, creating a Singleton manually is not common anymore.

That’s why I would like to revisit some of the design patterns in Modern Java, these article is based on the talk “Design Patterns Revisited in Modern Java by Venkat Subramaniam“.

Programming and Design Patterns to use

Optional

Handling null is a smell, that’s why one of the advice in the book Effective Java says, for collections “Do not return a null, instead return an empty collection”. We should extend that, and apply the same for any value. Therefore, when a method might or might not return a value, we should use the Optional.

It makes our code obvious, the caller of that method should not guess whether or not the method will return null , avoiding boiler and unnecessary lines for null checks.

public Optional<String> someMethod(){
  if(Math.random() > 0.5
     return Optional.of("Hello");
  }
  return Optional.empty();
}

public void myMethod(){
  var maybeResult = someMethod();
  //Some methods you can use.
  var result = maybeResult.orElse("Not found");
  maybeResult.ifPresentOrElse(() -> {/*some logic*/}, () -> {/*some other logic*/});
  System.out.println(result);
}

This article has more details about how to use the Java Optional.

Avoid these Optional anti-patterns

Please!!! avoid using the Optional.get() method, because it will return an exception if the Optional is empty, and then, we must use the Optional.isPresent(). So, we are just replacing the null-check for a non-empty check. Instead let’s use other methods like: orElse, orElseThrow, orElseGet, map.

Do not use Optional<T> as return type of a method that always return a single value. It makes no sense to add overhead on it, just return the value. The same applies for collections, do not wrap a collection in a Optional, just return an empty Collection.

Do not be tempted to use an Optional<T> as a parameter of a method. The call might still send null as the parameter, requiring to add null-checks. Also be empatic with other developers, if they have a String and you forces to send an Optional<String>, they might need to add extra-lines to wrap that value in an Optional.


Iterator Pattern with Stream

More than a design pattern, this is a programming pattern, some good practices to take advantages of new features in the language, and make our code more readable. In the past we use external iterators, where we control every step in the iteration, by using the while and for loops. We use statements like break or continue, and add logic into the block of code in the loops statements.

//Old approach 
int count = 0;
var myFruits = List.of("Apple", "Banana", "Watermelon", "Orange", "Pineapple", "Grape");
for(int i = 0; i < myList.size(); i++){
   var fruit = myFruits.get(i);
   if(fruit.lenght() > 5){
   		count++;
     	System.out.println(fruit.toUpperCase());
        if(count > 2){
         	break; 
        }
   }
}

However, Java provides a functional approach for iterating collections, is the Stream API, which helps the readability of our code, and reduce the amount of lines.

var myFruits = List.of("Apple", "Banana", "Watermelon", "Orange", "Pineapple", "Grape");
myFruits.stream()
  .filter(fruit -> fruit.lenght() > 5)
  .map(String::toUpperCase)
  .limit(2)
  .forEach(System.out::println);

Both snippet code makes the same. However, the one using the Stream API is more readable, and requires less lines of code to maintain. We are iterating a collection using an “internal iterator” provided by the language, indicating what is needed, and forgetting about the how.

Avoid these Stream Anti-Patterns

The Stream API is based on functional programming style, and functional programming relies on Expressions. Expressions relies on immutability of the data.

//AVOID THIS
var myFruits = List.of("Apple", "Banana", "Watermelon", "Orange", "Pineapple", "Grape");
var newList = new ArrayList<String>();
myFruits.stream()
  .filter(fruit -> fruit.lenght() > 5)
  .map(String::toUpperCase)
  .forEach(fruit -> newList.add(fruit));  //BAD IDEA

//FOLLOW THIS
var myFruits = List.of("Apple", "Banana", "Watermelon", "Orange", "Pineapple", "Grape");
var newList = myFruits.stream()
  .filter(fruit -> fruit.lenght() > 5)
  .map(String::toUpperCase)
  .toList();   //Best practice, use a collector to generate data from the stream

Also, the Stream Pipeline should be reproducible and not depend on attributes or values outside of the pipeline that might change its state at any moment. That’s why a Lambda Expression only can use final (or final in the practice) variables from the outside of the expression.

Functional Programming emphasizes immutability and purity, not because it is fashionable, but because it’s essential to its survival and efficiency.


Light-Weight Strategy Pattern

Let’s start by explaining what is the Strategy Pattern:

The Strategy Pattern allows to modify an small part of the algorithm in runtime while the rest of the algorithm is still the same.

In the past, we used the Strategy Pattern this way:

public interface PrinterStrategy {
  void print(List<Integer> numbers);
}

public class TotalValuePrinter implements PrinterStrategy{
  public void print(List<Integer> numbers){
    var total = 0;
    for(Integer number: numbers){
      total += number;
    }
    System.out.println(total);
  }
}

public class TotalEvenPrinter implements PrinterStrategy{
  public void print(List<Integer> numbers){
    var total = 0;
    for(Integer number: numbers){
      if(number % 2 == 0){
        total += number;
      }
    }
    System.out.println(total);
  }
}

public class TotalOddPrinter implements PrinterStrategy{
  public void print(List<Integer> numbers){
    var total = 0;
    for(Integer number: numbers){
      if(number % 2 != 0){
        total += number;
      }
    }
    System.out.println(total);
  }
}

public class MyService {
  private List<Integer> numberList;
  public PrinterContext(List<Integer> numberList){
    this.numberList = numberList;
  }
  public void process(PrinterStrategy strategy){
    //Some logic here
    strategy.print(numberList);
    //More logic here
  }
}

public class MyApp {
  public static void main(){
    var numberList = List.of(1,2,3,4,5,6,7,8,9);
    var myService = new MyService(numberList);
    
    myService.process(new TotalValuePrinter()); //Runs the process algorithm with the total value
    myService.process(new TotalEvenPrinter()); //Runs the process algorithm with even total value
    myService.process(new TotalOddPrinter()); //Runs the process algorithm with odd total value
  }
}

This is a very simple example of the Strategy Pattern, we have an interface, and some implementations. Then, there is a method that receives one implementation as parameter and that’s how the algorithm can be “modified” in runtime depending on the implementation. As we can see, it requires to write a lot of classes.

Using Lambda Expressions, we can reduce number of classes to just a few classes. A Strategy is in most of the cases a different implementation for one method, in those cases, Lambda Expression (the implementation of a Functional Interface) is a good use case.

public class MyService {
  private List<Integer> numberList;
  public PrinterContext(List<Integer> numberList){
    this.numberList = numberList;
  }
  public void process(Predicate<Integer> strategy){
    numberList.stream()
      .filter(predicate)
      .forEach(System.out::println);
  }
}

public class MyApp {
  public static void main(){
    var numberList = List.of(1,2,3,4,5,6,7,8,9);
    var myService = new MyService(numberList);
    
    myService.process(x => true); //Runs the algorithm for all the numbers
    myService.process(number => number % 2 == 0);//Runs the algorithm for even the numbers
    myService.process(number => number % 2 != 0);//Runs the algorithm for odd the numbers
  }
}

We are not say that Lambda Expression are replacing the original pattern, there are more complex scenarios (with more than 1 method) where a lambda expression is not suitable. But it provides a light-weight approach for a lot of scenarios.

Factory Method using default methods

Patterns can evolve with the time, taking advantage of new features provided by the language. In this opportunity we would use the default methods on interfaces (the same approach can work on abstract classes) to create a new Factory pattern, the “Factory Method Pattern”.

interface Person {
  Pet getPet();
 
  default void play(){
    System.out.println("Playing with "+ getPet());
  }
}

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

Let’s start with the interface Person which can play with a Pet , and we have different kind of pets, like Dog and Cat . So, the pattern begins with having a default method in the interface that defines how to play with a Pet, but the pet will depend on the Person implementation.

class DogPerson implements Person {
  	private Dog dog = new Dog();
 	Pet getPet(){ return dog; }
}
class CatLover implements Person {
  	private Cat cat = new Cat();
 	Pet getPet(){ return cat; }
}
public class Sample{
  public static void call(Person person){
    person.play();
  }
  
  public static void main(String [] args){
    call(new DogPerson()); //Prints -> Playing with Dog@1234
    call(new CatLover());  //Prints -> Playing with Cat@5678
  }
}

Now, the implementation of each Person uses the Pet it prefers. Without requiring any external class for it. Also, this same pattern can be applied using “abstract classes”, without interfaces and default method.

Abstract Factory vs Factory Method

This is not a replacement for other Factory methods, it’s just a new option that it’s more suitable to use in the Modern Java for some scenarios.

Factory Method: A class or an interface relies on a derived class to provide the implementation whereas the base provides the common behavior. It uses inheritance as a design tool.

Abstract Factory: It uses delegation as a design tool, you are passing parameter to another object.

So, both patterns are about Factoring instances for us, but the Abstract Factory relies on a class in charge of doing it. Factory Method relies on a method returning the instance, helping us to reduce code, we don’t need to create an independent class.

Laziness using Lambda Expressions

Laziness is a very powerful tool for delegating responsibility the code that will not execute immediately. So, it allows to make more efficient logic.

Lazy evaluation is to functional programming as Polymorphism is to Object Oriented programming.

In Computer Science we can solve almost any problem by introducing one more level of indirection.

David Wheeler

Let’s talk about indirection …

  • In Procedural code, like C, pointers give the power of indirection.
  • In Object Oriented code, like Java, overriding functions give the power of indirection.
    (because it’s on runtime, not compile time, that it’s decided which method to run depending on the implementation of the class)
  • In Functional code, like Scala, lambdas give the power of indirection.
    (because you can decided when to run your function, now, or just pass it as a parameter to another method to be executed later, so that logic is evaluated lazily).

Having said that, let use Lambda Expressions to provide Lazy evaluation in our code, and make our code more efficient.

public static int compute(int number){
  System.out.println("computing ...."); //Imagine it takes some time to compute
  return number * 100;
}

public static void main(String [] args){
  int value = 4; 
  int temp = compute(value);   //this is Eager, compute is evaluated right now
  if(value > 4 && temp > 100){
   	System.out.println("Path 1 with temp "+ temp); 
  } else {
  	System.out.println("Path 2");
  }
}

Running that logic, regardless of the value of the value variable, it will always execute the compute method, doing some heavy and unnecessary work in some scenarios. For example, if the value is 4 , temp will never be used, and the resource to get its value were wasted.

We can improve that logic with a few changes.

public Lazy<T> {
  private T instance;
  private Supplier<T> supplier;
  
  public Lazy(Supplier<T> supplier){
    this.supplier = supplier; 
  }
  
  public void get(){
    if(instance == null){
      instance = supplier.get();
    }
    return instance;
  }
}

public static int compute(int number){
  System.out.println("computing ...."); //Imagine it takes some time to compute
  return number * 100;
}

public static void main(String [] args){
  int value = 4; 
  Lazy<Integer> temp = new Lazy(() -> compute(value)); //This is lazy, not evaluated yet
  if(value > 4 && temp.get() > 100){ 
   	System.out.println("Path 1 with temp "+ temp); 
  } else {
  	System.out.println("Path 2");
  }
}

Now, temp value is not computed immediately. It’s just creating a lambda expression that is only executed in case the temp.get() method is called. So, we are not calling the compute method always, and we are saving some heavy tasks to be run.

It’s not something that we need every day, but it’s a useful approach to use in some specific cases. We can pass a Supplier or another Lambda Expression as parameter of methods, instead of the direct value.


Decorator Pattern using Lambda Expressions

Decorator pattern might be a “scary” pattern for many developers. We might have read about it, but it’s not usually used. This is an example of the decorator pattern, it looks “ugly”.

DataInputStream dis = new DataInputStream(
  new BufferedStream(
    new FileInputStream(....)));

The goal of the Decorator Pattern is to attach additional responsibilities to an object. That is possible through some interface, inheritance and composition of classes. In some cases the implementation of this pattern could be hard to understand at first seek. Let’s see an example:

interface Camera {
  Color snap();
}

class DefaultCameraImpl implements Camera {
  @Override
  public Color snap(Color input){
    return input;
  }
}

abstract class ColoredCamera implements Camera {
  private Camera camera;
  
  public ColoredCamera(Camera camera){
    this.camera = camera; 
  }
  @Override
  public Color snap(Color input){
    camera.snap();
  }
}

class BrighterCamera implements ColoredCamera {
  public BrighterCamera(Camera camera){
    super(camera);
  }
  @Override 
  public Color snap(Color input){
  	return super.snap(input).brigther();
  }
}

class DarkerCamera implements ColoredCamera {
  public DarkerCamera(Camera camera){
    super(camera);
  }
  @Override 
  public Color snap(Color input){
  	return super.snap(input).darker();
  }
}

And then, we can use that code in the following way.

public class MyApp {
  public static void main(String [] args){
   	Camera camera = new DarkerCamera(new BrighterCamera(new DefaultCameraImpl())); 
    Color newColor = camera.snap(new Color(125,125,125));
    System.out.println(newColor);
  }
}

Once it’s build, the Decorator Pattern is very useful and powerful, it’s about composing a chain of implementations adding new responsibilities dynamically. But, let’s be serious it requires a lot of classes to make it work.

What if we could get rid of those classes? and create a simpler approach with the same result. That can be done using the functional interface Function and Lambda Expressions.

public Camera {
  private Function<Color, Color> filters;
  public Camera(Function<Color, Color> ... filters){
    this.filters = Stream.of(filters)
      .reduce(Function.identity(), Function::andThen);
  }
  
  public Color snap(Color input){
    this.filters.apply(input);
  }
}

public class MyApp {
  public static void main(String [] args){
   	Camera camera = new Camera(Color::brighter, Color::darker); 
    Color newColor = camera.snap(new Color(125,125,125));
    System.out.println(newColor);
  }
}

Simpler approach, same result, using less classes and code. The key is using the method andThen for the Function interface to composite the functions, allowing to add more dynamically.

Let’s imagine you have a pipeline of data flowing in your application, you need to modify, validate, encrypt, decrypt, and other operations. This is a use case for the decorator pattern, you can do dynamically without restricting the steps or the order of them, and as a bonus it doesn’t look as scary as the approach using classes.

Fluent Interfaces

Fluent interfaces are in trend, because can create an object and execute very easily. It usually follows a Builder Pattern (more specifically, the method cascade pattern). Let’s see and example.

class Mailer {
  public Mailer from(String from){
  	System.out.println("From: "+from);
    return this;
  }
  
  public Mailer to(String to){
  	System.out.println("To: "+to);
    return this;
  }
  
  public Mailer subject(String subject){
  	System.out.println("Subject: "+subjet);
    return this;
  }
  
  public Mailer content(String content){
  	System.out.println("content: "+content);
    return this;
  }
  
  public void send(){
  	System.out.println("Sending ...");
  }
}

public class Sample {
  public static void main(String [] args){
    new Mailer()
      .from("builder@adamgamboa.dev")
      .to("adamgamboa@domain.dev")
      .subject("You can do it better")
      .content("This code is good, but it can be better")
      .send();
  }
}

That is a good example of a fluent code. But it has a disadvantage, is that new Mailer instance we did, we can save that in a variable, and be use later, should that be ok?. Let’s do some minor changes to avoid that situation.

class Mailer {
  
  private Mailer(){} //Private to avoid people creating instances
  
  public Mailer from(String from){
  	System.out.println("From: "+from);
    return this;
  }
  
  public Mailer to(String to){
  	System.out.println("To: "+to);
    return this;
  }
  
  public Mailer subject(String subject){
  	System.out.println("Subject: "+subjet);
    return this;
  }
  
  public Mailer content(String content){
  	System.out.println("content: "+content);
    return this;
  }
  
  public static void send(Consumer<Mailer> block){
    var mailer = new Mailer();
    block.accept(mailer);
  	System.out.println("Sending ...");
  }
}

public class Sample {
  public static void main(String [] args){
    Mailer.send(mailer ->
      mailer          
       .from("builder@adamgamboa.dev")
       .to("adamgamboa@domain.dev")
       .subject("This is better)
       .content("This code is good, you did it better"));
  }
}

So, developer are still using our code in a Fluent approach, but they don’t need to take care of creating an instance of Mailer any more.


Executed Around Method Pattern

This pattern is very useful when we have a resource that needs to be closed at the end of some operations.

class Resource implements AutoClosable {
  
  public void operation1(){
    System.out.println("operation 1");
  }
  
  public void operation2(){
    System.out.println("operation 2");
  }
  
  public void close(){
    System.out.println("closing.."); 
  }
}

public class Sample {
  public static void main(String[] args){
    try(Resource resource = new Resource()){
      resource.operation1();
      resource.operation2();
    }
  }
}

Using the ARM (Automatic Resource Management) feature in Java, with the Try-With-Resource, we can handle that. Because the Resource implements the AutoClosable interface, the try will call the close() method after all the operations, even if an exception happens.

But, that approach has a drawback, developers might be proned to forget executing the resource operations into the try-with-resources. Let’s see another approach that reduces that risk.

class Resource {
  
  private Resource(){} //Private to avoid instances
  
  public void operation1(){
    System.out.println("operation 1");
  }
  
  public void operation2(){
    System.out.println("operation 2");
  }
  
  private void close(){
    System.out.println("closing.."); 
  }
  
  public static use(Consumer<Resource> block){
    Resource resource = new Resource(); //Uses the private constructor
    try{
      block.accept(resource);
    } finally {
      resource.close();
    }
  }
}

public class Sample {
  public static void main(String[] args){
    Resource.use(resource -> {
      resource.operation1();
      resource.operation2();
    });
  }
}

A few of small changes. The constructor of resource is private, and we have a static method use receiving a block of code. That method will always handle the call to the close method. This approach is very useful for resources like Transactions, or implementing some kind of basic AOP programming.

Creating a Closed Hierarchy with Sealed

Sealed classes/interfaces are one of the new features provided by Java 17. Imagine, we are creating plugins or libraries for other people, you might want to provide interfaces for other people to use, but you don’t want other people to extend those interfaces on their side, that’s only for your internal usage.

That’s the reason for using Sealed classes, allow public accesibility but private extensibility.

public sealed interface TrafficLight {}

final class ReadLight implements TrafficLight {}
final class GreenLight implements TrafficLight {}
public Sample {
  public static void main(String [] args){
     TrafficLight light = new ReadLight(); //This is valid  
  }
}

But because, TrafficLight is sealed and ReadLight and GreenLight are final, our hierarchy is closed for extension. Other developers will not be able to extend from ReadLight, or GreenLight, neither to create a new implementation of TrafficLight.

I already wrote an article about it, take a look at it, for more details about Sealed Classes.


Conclusion

It’s good to revisit some of the patterns we can apply in our code, having new features added to the language. As we can see, taking advantage of those features, like lambda expression, we can simplify some of the existing patterns used in Java applications.

When using patterns, we need to use them only to resolve a problem, we don’t need to use them anywhere just because they are fancy. That can lead us to anti-patterns or make our logic overkill for simple scenarios. So, DONT FORCE the PATTERNS.

References

Advertisements

Leave a Reply

Your email address will not be published. Required fields are marked *