Structural Patterns

Introduction

These patters are about providing different ways to create a class structure with inheritance and composition.

All Patterns

Adapter

Adapter pattern wraps a interface in another interface that is required by the client, so it works as a bridge between to classes.

It is usually used when a specific interface is required by the client. This pattern can be applied when the target interface and the one required by the client are very similar.

The adapter class should do only the necessary to adapt the original class, so no additional logic should be added to the adapter.

Implementation

Adapter Pattern - UML
Adapter Pattern - UML
  1. Create adapter interface used by the client:

     public interface FullNameConverter {
         String toUpperCase(String firstName, String lastName);
     }
    
  2. Create adapter class implementing the adapter interface and adding a constructor with the adaptee:

     public class FullNameConverterAdapter implements FullNameConverter {
    
         private ExistingFullNameConverter existingFullNameConverter;
    
         public FullNameConverterAdapter(ExistingFullNameConverter existingFullNameConverter) {
             this.existingFullNameConverter = existingFullNameConverter;
         }
    
         @Override
         public String toUpperCase(String firstName, String lastName) {
             return existingFullNameConverter.toUpperCase(firstName + " " + lastName);
         }
     }
    
  3. The client will create an instance of the adapter class and inject the adaptee:

     FullNameConverter fullNameConverter = new FullNameConverterAdapter(new ExistingFullNameConverter());
    
     System.out.println(fullNameConverter.toUpperCase("Sergio" , "Martin"));
    

Proxy

Proxy pattern encapsulates an object to control references to it, so you can control the access to resources.

One of the use cases is when the object creation involves a heavy computation task, like calculation prime numbers. This pattern will allow you to lazily perform the computation only when the data is needed.

The Proxy pattern hides the behaviour of the original class. Therefore, the risk of using this pattern is that you might not be aware of what is happening under the hood.

Implementation

Proxy Pattern - UML
Proxy Pattern - UML
  1. Create interface for proxy class and original class:

     public interface PrimesCalculator {
         long getNumberOfPrimes();
     }
    
  2. Create original class and proxy class implementing the same interface:

     public class OriginalPrimesCalculator implements PrimesCalculator {
    
         private List<Long> primes;
    
         public OriginalPrimesCalculator(long n) {
             this.primes = LongStream.rangeClosed(2, n)
                     .filter(x -> isPrime(x))
                     .boxed()
                     .collect(Collectors.toList());
         }
    
         @Override
         public long getPrimesCount() {
             return primes.size();
         }
    
         private boolean isPrime(long number) {
             return LongStream.rangeClosed(2,  (long) Math.sqrt(number))
                     .allMatch(x -> number % x != 0);
         }
     }
    
     public class ProxyPrimeCalculator implements PrimesCalculator {
         private OriginalPrimesCalculator originalPrimesCalculator = null;
         private long number;
    
         public ProxyPrimeCalculator(long number) {
             this.number = number;
         }
            
         @Override
         public long getPrimesCount() {
             if (this.originalPrimesCalculator == null) {
                 this.originalPrimesCalculator = new OriginalPrimesCalculator(this.number);
             }
             return this.originalPrimesCalculator.getPrimesCount();
         }
     }
    
  3. Use the proxy class in the client:

     PrimesCalculator calculator = new ProxyPrimeCalculator(NUMBER);
     System.out.println("Proxy created!");
    
     long nPrimes = calculator.getPrimesCount();
     System.out.println("Number of Primes in " + NUMBER + " is " + nPrimes);
    

Decorator

Decorator pattern allows you to add additional functionality to an existing object without altering its structure.

This pattern is a great alternative to subclassing, because instead of using inheritance to provide new functionalities it uses composition to avoid producing an explosion of subclasses.

Keep in mind that many decorators can be hard to maintain and debug since the behaviour of the object changes at runtime.

Implementation

Decorator Pattern - UML
Decorator Pattern - UML
  1. Create an interface for the parent decorator classes:

     public interface Password {
         String getPassword(String value);
     }
    
  2. Create concrete classes implementing the interface:

     public class PlainTextPassword implements Password {
         @Override
         public String getPassword(String value) {
             return value;
         }
     }
    
  3. Create a parent decorator class implementing the interface:

     public class HashDecorator implements Password {
    
         private Password password;
    
         public HashDecorator(Password password) {
             this.password = password;
         }
    
         @Override
         public String getPassword(String value) {
             return password.getPassword(value);
         }
     }
    
  4. Create concrete decorator classes extending the parent decorator class.

     public class MD5Decorator extends HashDecorator {
    
         public MD5Decorator(Password password) {
             super(password);
         }
    
         @Override
         public String getPassword(String value) throws NoSuchAlgorithmException {
             MessageDigest messageDigest = MessageDigest.getInstance("MD5");
             messageDigest.update(value.getBytes());
             byte[] digest = messageDigest.digest();
             return super.getPassword(DatatypeConverter.printHexBinary(digest));
         }
     }
    

  5. Use specific decorators to decorate objects.

     Password password1 = new MD5Decorator(new PlainTextPassword());
     Password password2 = new SHA1Decorator(new PlainTextPassword());
     Password password3 = new BCryptDecorator(new PlainTextPassword());
     Password password4 = new MD5Decorator(new BCryptDecorator(new PlainTextPassword()));
     Password password5 = new SHA1Decorator(new MD5Decorator(new BCryptDecorator(new PlainTextPassword())));
    
     String hash1 = password1.getPassword("password");
     String hash2 = password2.getPassword("password");
     String hash3 = password3.getPassword("password");
     String hash4 = password4.getPassword("password");
     String hash5 = password5.getPassword("password");
    
     System.out.println("MD5: " + hash1);
     System.out.println("SHA-1: " + hash2);
     System.out.println("BCrypt: " + hash3);
     System.out.println("BCrypt of MD5: " + hash4);
     System.out.println("BCrypt of MD5 of SH1: " + hash5);
    

Bridge

Bridge pattern decouples abstraction from its implementation and avoids class explosion. This pattern give you the freedom of developing abstraction and implementation independently and create multiple combinations. The client only have access to the abstraction and does not need to know anything about the implementation.

On the other hand, Bridge pattern increases complexity due to the fact that it uses composition over inheritance

Implementation

Bridge Pattern - UML
Bridge Pattern - UML
  1. Create bridge implementer interface:

     public interface Difficulty {
         void play();
     }
    
  2. Create concrete bridge implementer classes implementing the previous interface:

     public class Easy implements Difficulty {
         @Override
         public void play() {
             System.out.println("EASY mode enabled");
         }
     }
    

  3. Create an abstract class that uses the interface:

     public abstract class Game {
         protected Difficulty difficulty;
    
         protected Game(Difficulty difficulty) {
             this.difficulty = difficulty;
         }
    
         public abstract void run();
     }
    
  4. Create concrete classes that inherit from the abstract class:

     public class Pong extends Game {
    
         public Pong(Difficulty difficulty) {
             super(difficulty);
         }
    
         @Override
         public void run() {
             difficulty.play();
         }
     }
    
  5. Use the implementation and abstraction classes on the client:

     Game minesweeper = new MinesSweeper(new Easy());
     minesweeper.run();
    
     Game pong = new Pong(new Hard());
     pong.run();
    
     Game arkanoid = new Arkanoid(new Medium());
     arkanoid.run();
    

Facade

Facade pattern provides an unified interface to a set of interfaces in a subsystem and is used to wraps complex systems. It provides a higher level interface that interacts with a complex subsystem.

Implementation

  1. Create an interface for the behaviours or actions:

     public interface Action {
         void doAction(UUID orderId);
     }
    
  2. Create concrete classes of each action implementing the previous interface:

     public class OrderCreation implements Action {
    
         @Override
         public void doAction(UUID orderId) {
             System.out.println("Creating order " + orderId + " on the system.");
         }
     }
    
     public class OrderPriceCalculator implements Action {
    
         @Override
         public void doAction(UUID orderId) {
             System.out.println("Calculation price for order " + orderId);
         }
     }   
    
     public class OrderProcessor implements Action {
    
         @Override
         public void doAction(UUID orderId) {
             System.out.println("Processing order " + orderId);
         }
     }
    
  3. Create facade class which encapsulates the different actions:

     public class OrderFacade {
    
         private OrderCreation orderCreation;
         private OrderPriceCalculator orderPriceCalculator;
         private OrderProcessor orderProcessor;
    
         public OrderFacade() {
             this.orderCreation = new OrderCreation();
             this.orderPriceCalculator = new OrderPriceCalculator();
             this.orderProcessor = new OrderProcessor();
         }
    
         public void createOrder(UUID orderId) {
             orderCreation.doAction(orderId);
         }
    
         public void calculateOrderPrice(UUID orderId) {
             orderPriceCalculator.doAction(orderId);
         }
    
         public void processOrder(UUID orderId) {
             orderProcessor.doAction(orderId);
         }
     }
    
  4. Use the facade to execute actions:

     OrderFacade orderFacade = new OrderFacade();
    
     orderFacade.createOrder(ORDER_ID);
     orderFacade.calculateOrderPrice(ORDER_ID);
     orderFacade.processOrder(ORDER_ID);