Essential Design Patterns in Java

Clifford Silla - Jan 26 '23 - - Dev Community

Design patterns are reusable solutions to common software design problems. They provide a way to organize and structure code in a consistent and efficient manner. Some common design patterns include:

  • The Factory pattern is a creational design pattern that provides an interface for creating objects in a super class, but allows subclasses to alter the type of objects that will be created.
  • The Abstract Factory pattern is a creational design pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes.
  • The Builder pattern is a creational design pattern that separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
  • The Strategy pattern is a behavioral design pattern that enables an algorithm's behavior to be selected at runtime.
  • The Decorator pattern is a structural design pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class.
  • The Singleton pattern is a creational design pattern that ensures that a class has only one instance, while also providing a global access point to this instance.
  • The Observer pattern is a behavioral design pattern that allows an object (the subject) to notify other objects (the observers) when its state changes.

These patterns are useful because they provide a common language for developers and can make code more maintainable and understandable.

Below are 6 of the most commonly used design patterns with examples in java.

Factory Pattern

Factory

The factory pattern is a creational design pattern that provides a way to create objects without specifying the exact class of object that will be created. It allows a class to delegate the responsibility of creating objects to its sub-classes.

Here's an example of how the factory pattern can be implemented in Java:

interface Shape {
    void draw();
}

class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}

class Square implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a square");
    }
}

class ShapeFactory {
    public Shape getShape(String shapeType) {
        if (shapeType == null) {
            return null;
        }
        if (shapeType.equalsIgnoreCase("RECTANGLE")) {
            return new Rectangle();
        } else if (shapeType.equalsIgnoreCase("SQUARE")) {
            return new Square();
        }
        return null;
    }
}
Enter fullscreen mode Exit fullscreen mode

In the above example, the ShapeFactory class is the factory class that creates objects of different concrete classes (Rectangle and Square) based on the input provided by the client. The client uses the factory class to create objects and doesn't need to know the specific class of object that will be created.

Here's an example of how the client code would use the factory pattern to create objects:

ShapeFactory shapeFactory = new ShapeFactory();

//get an object of Rectangle and call its draw method.
Shape shape1 = shapeFactory.getShape("RECTANGLE");

//call draw method of Rectangle
shape1.draw();

//get an object of Square and call its draw method.
Shape shape2 = shapeFactory.getShape("SQUARE");

//call draw method of square
shape2.draw();

Enter fullscreen mode Exit fullscreen mode

In this example, the client can create different shape objects by providing different inputs to the factory class, without needing to know the specific class of object that will be created. This allows for flexibility in the code and makes it easier to add new classes without modifying the existing code.

Builder Pattern

Builder Pattern

The builder pattern is a creational design pattern that allows for the construction of complex objects to be done step-by-step in a clear and organized manner. It separates the construction of an object from its representation, making it easier to change the internal representation of an object without affecting the client code.

Here's an example of how the builder pattern can be implemented in Java:

class Computer {
    private String CPU;
    private String RAM;
    private String GPU;
    private String storage;

    private Computer(ComputerBuilder builder) {
        this.CPU = builder.CPU;
        this.RAM = builder.RAM;
        this.GPU = builder.GPU;
        this.storage = builder.storage;
    }

    public String getCPU() {
        return CPU;
    }

    public String getRAM() {
        return RAM;
    }

    public String getGPU() {
        return GPU;
    }

    public String getStorage() {
        return storage;
    }

    public static class ComputerBuilder {
        private String CPU;
        private String RAM;
        private String GPU;
        private String storage;

        public ComputerBuilder setCPU(String CPU) {
            this.CPU = CPU;
            return this;
        }

        public ComputerBuilder setRAM(String RAM) {
            this.RAM = RAM;
            return this;
        }

        public ComputerBuilder setGPU(String GPU) {
            this.GPU = GPU;
            return this;
        }

        public ComputerBuilder setStorage(String storage) {
            this.storage = storage;
            return this;
        }

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

Enter fullscreen mode Exit fullscreen mode

In this example, the Computer class represents the complex object that is being built, and the ComputerBuilder class is the builder class that constructs the Computer object step-by-step. The ComputerBuilder class has setter methods for each of the fields in the Computer class, and a build() method that returns the constructed Computer object.

Here's an example of how the client code would use the builder pattern to create a Computer object:

Computer computer = new Computer.ComputerBuilder()
                .setCPU("i7")
                .setRAM("16GB")
                .setGPU("GTX 1080")
                .setStorage("1TB")
                .build();

Enter fullscreen mode Exit fullscreen mode

In this example, the client can create a Computer object by providing different values for each of the fields, in a step-by-step manner, using the builder class. This makes the code more readable and easy to understand, as the client does not have to worry about the internal representation of the Computer object.

Strategy Design Pattern

The strategy pattern is a behavioral design pattern that allows for the selection of an algorithm at runtime. It defines a family of algorithms, encapsulates each one, and makes them interchangeable. This allows the algorithm to vary independently from the clients that use it.

Here's an example of how the strategy pattern can be implemented in Java:

interface PaymentStrategy {
    void pay(int amount);
}

class CreditCardStrategy implements PaymentStrategy {
    private String name;
    private String cardNumber;
    private String cvv;
    private String dateOfExpiry;

    public CreditCardStrategy(String nm, String ccNum, String cvv, String expiryDate){
        this.name=nm;
        this.cardNumber=ccNum;
        this.cvv=cvv;
        this.dateOfExpiry=expiryDate;
    }
    @Override
    public void pay(int amount) {
        System.out.println(amount + " paid with credit/debit card");
    }
}

class PayPalStrategy implements PaymentStrategy {
    private String emailId;
    private String password;

    public PayPalStrategy(String email, String pwd){
        this.emailId=email;
        this.password=pwd;
    }
    @Override
    public void pay(int amount) {
        System.out.println(amount + " paid using PayPal.");
    }
}

class ShoppingCart {
    List<Item> items;
    PaymentStrategy paymentStrategy;

    public ShoppingCart(){
        this.items=new ArrayList<Item>();
    }

    public void addItem(Item item){
        this.items.add(item);
    }

    public void removeItem(Item item){
        this.items.remove(item);
    }

    public int calculateTotal(){
        int sum = 0;
        for(Item item : items){
            sum += item.getPrice();
        }
        return sum;
    }

    public void pay(){
        int amount = calculateTotal();
        paymentStrategy.pay(amount);
    }

    public void setPaymentStrategy(PaymentStrategy paymentMethod){
        this.paymentStrategy=paymentMethod;
    }
}

Enter fullscreen mode Exit fullscreen mode

In this example, the PaymentStrategy interface defines the pay method that is used to make a payment. The CreditCardStrategy and PayPalStrategy classes are concrete implementations of this interface that provide different ways to make a payment (i.e. using credit/debit card or PayPal). The ShoppingCart class is the client that uses the PaymentStrategy interface to make a payment. The ShoppingCart class can set the payment strategy at runtime using the setPaymentStrategy method.

Here's an example of how the client code would use the strategy pattern:

ShoppingCart cart = new ShoppingCart();
cart.addItem(new Item("item1", 100));
cart.addItem(new Item("item2", 50));

// Selecting the CreditCardStrategy
cart.setPaymentStrategy(new CreditCardStrategy("John Doe","1234567890123456", "123", "12/2022"));
cart.pay();

// Selecting the PayPalStrategy
cart.setPaymentStrategy(new PayPalStrategy("test@example.com", "password"));
cart.pay();

Enter fullscreen mode Exit fullscreen mode

In this example, the client creates an instance of the ShoppingCart class and adds some items to it. Then, the client selects the payment strategy by setting it on the ShoppingCart object using the setPaymentStrategy method. This can be done at runtime, so the client can switch between different payment strategies as needed. Finally, the client calls the pay() method on the ShoppingCart object to make the payment using the selected strategy.

In this example the client code can switch between credit card and PayPal payment methods, but it could also include other types of payment methods such as bank transfer, etc. With this pattern the client code does not need to worry about the details of each payment method, it only needs to know about the common PaymentStrategy interface and that the concrete classes that implement it are able to pay.

Decorator Pattern

Decorator Pattern

The decorator pattern is a structural design pattern that allows for the dynamic addition of behavior to an individual object, either by wrapping it with a decorator object or by extending it with additional functionality. The decorator pattern allows for the creation of flexible and reusable code.

Here's an example of how the decorator pattern can be implemented in Java:

interface Shape {
    void draw();
}

class Circle implements Shape {
    public void draw() {
        System.out.println("Drawing Circle");
    }
}

abstract class ShapeDecorator implements Shape {
    protected Shape decoratedShape;

    public ShapeDecorator(Shape decoratedShape){
        this.decoratedShape = decoratedShape;
    }

    public void draw(){
        decoratedShape.draw();
    }
}

class RedShapeDecorator extends ShapeDecorator {
    public RedShapeDecorator(Shape decoratedShape) {
        super(decoratedShape);
    }

    @Override
    public void draw() {
        decoratedShape.draw();
        setRedBorder(decoratedShape);
    }

    private void setRedBorder(Shape decoratedShape){
        System.out.println("Border Color: Red");
    }
}

Enter fullscreen mode Exit fullscreen mode

In this example, the Shape interface defines the draw() method that is used to draw a shape. The Circle class is a concrete implementation of this interface that provides the behavior for drawing a circle. The ShapeDecorator is an abstract class that also implements the Shape interface, but it has an additional decoratedShape attribute that references the shape it is decorating. The RedShapeDecorator class is a concrete decorator that adds a red border to the shape it decorates.

Here's an example of how the client code would use the decorator pattern:

Shape circle = new Circle();
Shape redCircle = new RedShapeDecorator(new Circle());

circle.draw();
redCircle.draw();

Enter fullscreen mode Exit fullscreen mode

In this example, the client creates an instance of the Circle class, and then creates an instance of the RedShapeDecorator class, passing the Circle instance as an argument. The RedShapeDecorator class wraps the Circle instance and adds the additional behavior of drawing a red border. The client can then call the draw() method on both the circle and redCircle objects, and the behavior of the Circle class will be decorated with the additional behavior provided by the RedShapeDecorator class.

In this example, we used the decorator pattern to add the red border to the shape , but you can use this pattern to add or modify other features or behavior to the decorated object. The key of this pattern is that the client does not need to know about the decorator classes, it only needs to know about the interface of the decorated object.

Singleton Pattern

The singleton pattern is a creational design pattern that ensures that a class has only one instance, while also providing a global access point to this instance. The singleton pattern is used in situations where a single instance of a class must control the action throughout the execution.

Here's an example of how the singleton pattern can be implemented in Java:

class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    public void doSomething() {
        // some code
    }
}

Enter fullscreen mode Exit fullscreen mode

In this example, the Singleton class has a private constructor, which means that the class cannot be instantiated from outside the class. The class also has a static instance attribute, which will hold the single instance of the class. The getInstance() method is used to get the single instance of the class, and it uses lazy initialization to create the instance only when it is first needed.

Here's an example of how the client code would use the singleton pattern:

Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();

System.out.println(singleton1 == singleton2); // true

Enter fullscreen mode Exit fullscreen mode

In this example, the client code calls the getInstance() method twice, but it only gets one instance of the Singleton class. The == operator is used to compare the references of the two instances, and it returns true because they are the same instance.

The singleton pattern is useful when you want to make sure that a class is instantiated only once and that there is a single instance of it that is globally accessible. It is also useful when you want to control the resources or the number of instances of a class and you want to enforce a single point of control over it.

It is important to note that the singleton pattern is not thread-safe by default. If multiple threads access the getInstance() method simultaneously, multiple instances of the Singleton class may be created. To make the singleton pattern thread-safe, you can use the synchronized keyword on the getInstance method or you can use the double-checked locking pattern.

Observer Pattern

Observer Pattern
The observer pattern is a behavioral design pattern that allows an object (the subject) to notify other objects (the observers) when its state changes. The observer pattern is used in situations where one object needs to be informed of changes to the state of another object, without the two objects having a direct reference to each other.

Here's an example of how the observer pattern can be implemented in Java:

interface Observer {
    void update(String message);
}

class ConcreteObserver implements Observer {
    public void update(String message) {
        System.out.println("Received message: " + message);
    }
}

interface Subject {
    void registerObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
}

class ConcreteSubject implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private String message;

    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(message);
        }
    }

    public void setMessage(String message) {
        this.message = message;
        notifyObservers();
    }
}

Enter fullscreen mode Exit fullscreen mode

In this example, the Observer interface defines the update() method that will be called when the state of the subject changes. The ConcreteObserver class is a concrete implementation of the Observer interface that provides the behavior for handling the update.

The Subject interface defines the methods for registering and removing observers, as well as for notifying all registered observers when the state of the subject changes. The ConcreteSubject class is a concrete implementation of the Subject interface that maintains a list of observers and provides the methods for registering and removing observers, as well as for notifying all registered observers when the state of the subject changes.

Here's an example of how the client code would use the observer pattern:

ConcreteSubject subject = new ConcreteSubject();
Observer observer = new ConcreteObserver();

subject.registerObserver(observer);
subject.setMessage("Hello World!");

Enter fullscreen mode Exit fullscreen mode

In this example, the client code creates an instance of the ConcreteSubject class and an instance of the ConcreteObserver class. The observer is then registered with the subject using the registerObserver() method. The client then sets the message of the subject using the setMessage()

In conclusion, Design patterns are an essential part of software development, they help developers to solve common problems and make the code more maintainable, reusable and scalable. Understanding and using design patterns can help developers to write better and more efficient code. It is important to note that design patterns are not a one-size-fits-all solution, and it's essential to understand the problem and context before applying a specific pattern.


This article was created with the help of AI

. . .