Design patterns

Design patterns are proven solutions to common problems that arise during system design. There are three types: creational, structural, and behavioral. In this article, I will present three creational patterns – Factory, Builder, and Strategy.

Factory

This pattern is useful in situations where we often need to create objects from classes that share a common interface or superclass. It is especially helpful when object creation is complex or requires many different arguments for the constructor, depending on the type of object being created. Below is an example for shapes.

public interface Shape {
    String getName();
    double getArea();
    double getCircumference();
}

public class Circle implements Shape {
    private String name;
    private double radius;
    public Circle(String name, final double radius) {
        this.name = name;
        this.radius = radius;
    }
    String getName(){
        return this.name;    }
    public double getArea() {
        return Math.PI * Math.pow(radius, 2.0);   }

    public double getCircumference() {
        return 2 * Math.PI * radius;   }
}

public class Square implements Shape {
    private String name;
    private double width;

    public Square(String name, double width) {
        this.name = name;
        this.width = width;    }
    public String getName(){
        return this.name;        }
    public double getArea() {
        return Math.pow(width, 2.0);    }

    public double getCircumference() {
        return 4 * width;    }
}

public  class Rectangle implements Shape {
    private String name;
    private double width;
    private double length;

    public Rectangle(double width, double length) {
        this.width = width;
        this.length = length;
    }
    public String getName(){
        return this.name;        }
    public double getArea() {
        return width * length;
    }
    
    public double getCircumference() {
        return 2 * width + 2 * length;
    }
}

If we wanted to create many objects implementing the Shape interface, we would have to provide arguments in the constructor each time. In our example, there are only 2 or 3 arguments, so it wouldn’t be a big problem, but if we wanted to add, for example, 5–10 more parameters to each class, it would become an issue. This is where the Factory pattern comes to the rescue.

First, we define an enum specifying the types of objects we want to create.

public enum ShapeType {
    CIRCLE,RECTANGLE,SQUARE
}

Now, let’s create our factory. We can use a static method to avoid the need to instantiate the factory.

public class ShapeFactory {
    public static Shape makeShape(ShapeType type) {
        return switch (type) {
            case CIRCLE -> new Circle("Circle", 10.50);
            case SQUARE -> new Square("Square", 17.0);
            case RECTANGLE -> new Rectangle("Rectange", 12.0, 4.50);
            default ->  throw new UnsupportedOperationException("Unsupported ShapeType");
        };
    }
}

Builder

This is another creational pattern. It is used when dealing with a large number of constructor parameters or optional parameters. The pattern breaks the entire object creation process into stages, making it possible to create an object in different configurations. We can create various objects using the same construction code.

This pattern utilizes an inner public static class that has access to a private constructor.

public class Bigmac {
    private String bun;
    private int burgers;
    private String souce;
    private List<String> ingredients;

    public static class BigmacBuilder {
        private String bun;
        private int burgers;
        private String souce;
        private List<String> ingredients = new ArrayList<>();

        public BigmacBuilder bun(String bun) {
            this.bun = bun;
            return this;
        }

        public BigmacBuilder burgers(int burgers) {
            this.burgers = burgers;
            return this;
        }

        public BigmacBuilder souce(String souce) {
            this.souce = souce;
            return this;
        }

        public BigmacBuilder addIngredient(String ingredient) {
            this.ingredients.add(ingredient);
            return this;
        }

        public Bigmac build() {
            return new Bigmac(this);
        }
    }

    private Bigmac(BigmacBuilder builder) {
        this.bun = builder.bun;
        this.burgers = builder.burgers;
        this.souce = builder.souce;
        this.ingredients = builder.ingredients;
    }
}    

A poniżej przykład użycia.

public static void main(String[] args) {
    Bigmac bigmac = new Bigmac.BigmacBuilder()
            .souce("amerykanski")
            .burgers(2)
            .bun("sezam")
            .addIngredient("salata")
            .addIngredient("pomidor")
            .build();
}

Strategy

This pattern is based on „injecting” entire pieces of code that perform specific tasks, depending on the chosen strategy. It allows for code separation by placing it in separate classes, which are linked by a common interface or superclass.

We’ll demonstrate this with an example of a bank investment client who buys investment products and is profiled based on their chosen strategy.

First, let’s define an interface and the classes that implement it.

public interface Strategy{
    void whatToBuy();
}
class AggressiveStrategy implements Strategy{
    public void buyActionAndObligation() {
        System.out.println("Kupujemy agresywnie");
    }
}
class BalancedStrategy implements Strategy{
public void buyActionAndObligation() {
    System.out.println("Kupujemy w balansie: akcje i obligacje");
   }
}
class ConservativeStrategy implements Strategy{
    public void buyActionAndObligation() {
        System.out.println("Kupujemy same obligacje o ratingu A++");
    }
}

Now, we create the Customer class, where we define a Strategy field. We also add a setter for the strategy field so that the client can change their investment strategy if they wish.

public class Customer{
    String name;
    Strategy strategy;
    public Customer(String name, Strategy strategy) {
        this.name = name;
        this.strategy = strategy;
    }
    public void buyActionAndObligation(){
        strategy.buyActionAndObligation();
    }
    void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }
}