Design Patterns Ultimate Guide

You might think, "Oh no, not another blog post on design patterns! There are already so many of them!" But don't worry, dear reader, this blog post is different. Our journey will be an exciting adventure filled with captivating before-and-after transformations, as we reveal the intriguing secrets of design patterns in a fun and easy-to-understand way!

Just like an expert who knows how to turn chaos into order, we'll demonstrate the power of design patterns to transform your messy, tangled code into elegant, efficient solutions. So, get prepared, and join us as we uncover the remarkable techniques of Java Design Patterns and their striking before-and-after transformations.

Together, we'll delve into the world of Creational, Structural, and Behavioral patterns, with real-world examples and amusing anecdotes to help you effortlessly grasp these essential concepts. By the end of our exciting journey, you'll have the ability to create cleaner, more maintainable, and scalable code in no time!

So, without further delay, let's embark on our journey through the captivating landscape of Java Design Patterns. Are you ready for an unforgettable adventure? Let the exploration begin!


Factory Pattern

Instead of calling a constructor to create an object directly, the Factory Pattern is used to create objects based on specified criteria. Let's dive into a simple example to illustrate the Factory Pattern with before and after code snippets.

public class Vehicle {

    private String type;

    public Vehicle(String type) {
        this.type = type;
    }

    public void drive() {
        if (type.equals("car")) {
            System.out.println("Driving a car");
        } else if (type.equals("truck")) {
            System.out.println("Driving a truck");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Vehicle car = new Vehicle("car");
        car.drive();

        Vehicle truck = new Vehicle("truck");
        truck.drive();
    }
}

In the "before" example, the Vehicle class handles different vehicle types using a string attribute type. This approach is not very flexible, as adding more vehicle types requires modifying the Vehicle class.

In the "before" example, the Vehicle class handles different vehicle types using a string attribute type. This approach is not very flexible, as adding more vehicle types requires modifying the Vehicle class.

// Vehicle interface
public interface Vehicle {
    void drive();
}

// Car class implementing Vehicle interface
public class Car implements Vehicle {
    public void drive() {
        System.out.println("Driving a car");
    }
}

// Truck class implementing Vehicle interface
public class Truck implements Vehicle {
    public void drive() {
        System.out.println("Driving a truck");
    }
}

// VehicleFactory class
public class VehicleFactory {
    public static Vehicle createVehicle(String type) {
        if (type.equals("car")) {
            return new Car();
        } else if (type.equals("truck")) {
            return new Truck();
        } else {
            throw new IllegalArgumentException("Invalid vehicle type");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Vehicle car = VehicleFactory.createVehicle("car");
        car.drive();

        Vehicle truck = VehicleFactory.createVehicle("truck");
        truck.drive();
    }
}

In the "after" example, we introduce the Factory Pattern by creating a VehicleFactory class that is responsible for creating objects based on the input type. We also define a Vehicle interface and create Car and Truck classes that implement this interface. This approach makes it easier to add more vehicle types without modifying existing classes.

By using the Factory Pattern, we've separated the responsibility of object creation and made the code more maintainable and scalable.

A factory method does come with certain limitations. The primary constraint is that it can only be applied to a specific group of related objects. This implies that all classes should share similar properties or have a common foundation.


Abstract Factory Pattern

The Abstract Factory Pattern focuses on creating families of related objects.

Let's consider a scenario where we have different categories of vehicles, such as electric vehicles and gas vehicles, with each category having its own types (e.g., electric cars, electric trucks, gas cars, and gas trucks). In this case, the Abstract Factory Pattern is more suitable as it deals with families of related objects.

// Vehicle interface
public interface Vehicle {
    void drive();
}

// ElectricCar class
public class ElectricCar implements Vehicle {
    public void drive() {
        System.out.println("Driving an electric car");
    }
}

// ElectricTruck class
public class ElectricTruck implements Vehicle {
    public void drive() {
        System.out.println("Driving an electric truck");
    }
}

// GasCar class
public class GasCar implements Vehicle {
    public void drive() {
        System.out.println("Driving a gas car");
    }
}

// GasTruck class
public class GasTruck implements Vehicle {
    public void drive() {
        System.out.println("Driving a gas truck");
    }
}

// Abstract factory interface
public interface VehicleFactory {
    Vehicle createCar();
    Vehicle createTruck();
}

// ElectricVehicleFactory class
public class ElectricVehicleFactory implements VehicleFactory {
    public Vehicle createCar() {
        return new ElectricCar();
    }

    public Vehicle createTruck() {
        return new ElectricTruck();
    }
}

// GasVehicleFactory class
public class GasVehicleFactory implements VehicleFactory {
    public Vehicle createCar() {
        return new GasCar();
    }

    public Vehicle createTruck() {
        return new GasTruck();
    }
}

With the Abstract Factory Pattern, we create an abstract VehicleFactory interface and two concrete factories, ElectricVehicleFactory and GasVehicleFactory, each responsible for creating a family of related objects.


Builder Pattern

Behind the builder pattern is to construct complex instances without polluting the constructor.

Suppose we have a Person class with several optional fields. Using constructors or setters to create a Person object can lead to a lot of boilerplate code and complexity.

public class Person {
    private String firstName;
    private String lastName;
    private int age;
    private String address;
    private String phoneNumber;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person("John", "Doe");
        person.setAge(30);
        person.setAddress("123 Main Street");
        person.setPhoneNumber("555-1234");
    }
}

In the "before" example, we use constructors and setters to create a Person object. This approach can become unwieldy when the number of fields increases.

public class Person {
    private String firstName;
    private String lastName;
    private int age;
    private String address;
    private String phoneNumber;

    private Person(Builder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
        this.address = builder.address;
        this.phoneNumber = builder.phoneNumber;
    }

    public static class Builder {
        private String firstName;
        private String lastName;
        private int age;
        private String address;
        private String phoneNumber;

        public Builder(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        public Builder age(int age) {
            this.age = age;
            return this;
        }

        public Builder address(String address) {
            this.address = address;
            return this;
        }

        public Builder phoneNumber(String phoneNumber) {
            this.phoneNumber = phoneNumber;
            return this;
        }

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

public class Main {
    public static void main(String[] args) {
        Person person = new Person.Builder("John", "Doe")
                .age(30)
                .address("123 Main Street")
                .phoneNumber("555-1234")
                .build();
    }
}

In the "after" example, we introduce the Builder Pattern by creating a nested Builder class within the Person class. This allows us to construct a Person object step by step, making the code more readable and maintainable.

By using the Builder Pattern, we've made it easier to construct complex objects and eliminated the need for multiple constructors or a large number of setters, making the code more maintainable and flexible.


Prototype Pattern

Prototype design pattern is used when the Object creation is a costly affair and requires a lot of time and resources and you have a similar object already existing.

import java.util.ArrayList;
import java.util.List;

public class Employees implements Cloneable{

	private List<String> empList;
	
	public Employees(){
		empList = new ArrayList<String>();
	}
	
	public Employees(List<String> list){
		this.empList=list;
	}
	public void loadData(){
		//read all employees from database and put into the list
		empList.add("Pankaj");
		empList.add("Raj");
		empList.add("David");
		empList.add("Lisa");
	}
	
	public List<String> getEmpList() {
		return empList;
	}

	@Override
	public Object clone() throws CloneNotSupportedException{
			List<String> temp = new ArrayList<String>();
			for(String s : this.getEmpList()){
				temp.add(s);
			}
			return new Employees(temp);
	}
	
}

Structural patterns

Structural patterns strive to clarify the relationships between instances, aiming not only to maintain the application but also to make it easier to comprehend its purpose.

Adapter Pattern

It referred to as the Wrapper, involves wrapping the behavior of an adaptee (the connected class) so it can be accessed without modification through an existing interface. Typically, the adaptee uses an incompatible interface. The Adapter Pattern streamlines the adaptee's behavior and seamlessly offers access to the necessary functionality.


Bridge Pattern

Let's consider a scenario where you are building a graphical application that allows users to draw different shapes with various colors. Without using the Bridge pattern, your implementation might look like this:


public abstract class Shape {
    public abstract void draw();
}

Create concrete Shape subclasses for each shape-color combination:


public class RedCircle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a red circle");
    }
}

public class BlueCircle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a blue circle");
    }
}

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

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

In this implementation, you need to create a separate class for each shape-color combination. As you add more shapes and colors, the number of classes will grow exponentially, leading to a large, difficult-to-maintain codebase.

After Bridge Pattern:

Using the Bridge pattern, you can separate the shape and color concerns into different hierarchies, allowing them to vary independently.

  1. Create a Color interface:

public interface Color {
    void applyColor();
}

Implement the Color interface:


public class RedColor implements Color {
    @Override
    public void applyColor() {
        System.out.println("Applying red color");
    }
}

public class BlueColor implements Color {
    @Override
    public void applyColor() {
        System.out.println("Applying blue color");
    }
}

Modify the abstract Shape class to include a reference to the Color interface:


public abstract class Shape {
    protected Color color;

    public Shape(Color color) {
        this.color = color;
    }

    public abstract void draw();
}

Modify the concrete Shape subclasses:

 
public class Circle extends Shape {
    public Circle(Color color) {
        super(color);
    }

    @Override
    public void draw() {
        System.out.print("Drawing a circle in ");
        color.applyColor();
    }
}

public class Rectangle extends Shape {
    public Rectangle(Color color) {
        super(color);
    }

    @Override
    public void draw() {
        System.out.print("Drawing a rectangle in ");
        color.applyColor();
    }
}

Now, you have decoupled the shape and color hierarchies, allowing them to vary independently. You can add new shapes or colors without creating an exponential number of new classes. The Bridge pattern has made the codebase more modular, flexible, and maintainable.


Composite pattern

It that allows you to compose objects into tree structures to represent part-whole hierarchies. It enables clients to treat individual objects and compositions of objects uniformly. The Composite pattern is especially useful when you need to work with a hierarchy of objects that have a similar structure but varying complexity.

Let's consider a scenario where you are building a file system application that represents files and directories. Without using the Composite pattern, your implementation might look like this:

Before Composite Pattern:

Create separate classes for File and Directory:


public class File {
    private String name;

    public File(String name) {
        this.name = name;
    }

    public void display() {
        System.out.println("File: " + name);
    }
}

public class Directory {
    private String name;
    private List<File> files = new ArrayList<>();

    public Directory(String name) {
        this.name = name;
    }

    public void addFile(File file) {
        files.add(file);
    }

    public void display() {
        System.out.println("Directory: " + name);
        for (File file : files) {
            file.display();
        }
    }
}

Use the File and Directory classes in your client code:


public class Client {
    public static void main(String[] args) {
        File file1 = new File("file1.txt");
        File file2 = new File("file2.txt");

        Directory directory = new Directory("Documents");
        directory.addFile(file1);
        directory.addFile(file2);

        file1.display();
        directory.display();
    }
}

In this implementation, the File and Directory classes have different interfaces, which makes it difficult to treat them uniformly in the client code.

After Composite Pattern:

Using the Composite pattern, you can represent both files and directories using a common interface, making it easier to treat them uniformly in the client code.

Create a common interface for File and Directory:


public interface FileSystemComponent {
    void display();
}

Implement the FileSystemComponent interface in both the File and Directory classes:


public class File implements FileSystemComponent {
    private String name;

    public File(String name) {
        this.name = name;
    }

    @Override
    public void display() {
        System.out.println("File: " + name);
    }
}

public class Directory implements FileSystemComponent {
    private String name;
    private List<FileSystemComponent> components = new ArrayList<>();

    public Directory(String name) {
        this.name = name;
    }

    public void addComponent(FileSystemComponent component) {
        components.add(component);
    }

    @Override
    public void display() {
        System.out.println("Directory: " + name);
        for (FileSystemComponent component : components) {
            component.display();
        }
    }
}

Use the modified File and Directory classes in your client code:


public class Client {
    public static void main(String[] args) {
        FileSystemComponent file1 = new File("file1.txt");
        FileSystemComponent file2 = new File("file2.txt");

        Directory directory = new Directory("Documents");
        directory.addComponent(file1);
        directory.addComponent(file2);

        file1.display();
        directory.display();
    }
}

Decorator Pattern

The decorator pattern provides the ability to add new functionality to objects by placing those objects in a decorator, So that a decorated instance provides extended functionality.

Let's consider a scenario where you are building a coffee shop application that sells different types of coffee with optional add-ons like milk, whipped cream, and chocolate. Without using the Decorator pattern, your implementation might look like this:

Before Decorator Pattern:

Create a Coffee class with methods to add add-ons:


public class Coffee {
    private double cost = 1.0;
    private String description = "Coffee";

    public void addMilk() {
        cost += 0.5;
        description += ", Milk";
    }

    public void addWhippedCream() {
        cost += 0.7;
        description += ", Whipped Cream";
    }

    public void addChocolate() {
        cost += 0.6;
        description += ", Chocolate";
    }

    public double getCost() {
        return cost;
    }

    public String getDescription() {
        return description;
    }
}

Use the Coffee class in your client code:


public class Client {
    public static void main(String[] args) {
        Coffee coffee = new Coffee();
        coffee.addMilk();
        coffee.addWhippedCream();
        System.out.println(coffee.getDescription() + " $" + coffee.getCost());
    }
}

In this implementation, the Coffee class is responsible for managing its add-ons, which makes it difficult to maintain, modify, or extend with new add-ons.

After Decorator Pattern:

Using the Decorator pattern, you can separate the coffee and add-on concerns into different classes, making it easier to maintain, modify, and extend.

Create a Beverage abstract class:


public abstract class Beverage {
    public abstract double getCost();
    public abstract String getDescription();
}

Implement the Beverage class for Coffee:


public class Coffee extends Beverage {
    @Override
    public double getCost() {
        return 1.0;
    }

    @Override
    public String getDescription() {
        return "Coffee";
    }
}

Create an abstract BeverageDecorator class that extends Beverage:


public abstract class BeverageDecorator extends Beverage {
    protected Beverage beverage;

    public BeverageDecorator(Beverage beverage) {
        this.beverage = beverage;
    }
}

Implement the BeverageDecorator for different add-ons:


public class MilkDecorator extends BeverageDecorator {
    public MilkDecorator(Beverage beverage) {
        super(beverage);
    }

    @Override
    public double getCost() {
        return beverage.getCost() + 0.5;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", Milk";
    }
}

public class WhippedCreamDecorator extends BeverageDecorator {
    public WhippedCreamDecorator(Beverage beverage) {
        super(beverage);
    }

    @Override
    public double getCost() {
        return beverage.getCost() + 0.7;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", Whipped Cream";
    }
}

Use the modified Coffee and decorator classes in your client code:


public class Client {
    public static void main(String[] args) {
        Beverage coffee = new Coffee();
        coffee = new MilkDecorator(coffee);
        coffee = new WhippedCreamDecorator(coffee);
        System.out.println(coffee.getDescription() + " $" + coffee.getCost());
    }
}

Now, the coffee and add-on concerns are separated into different classes, making it easier to maintain, modify, and extend with new add-ons. The Decorator pattern has made the codebase more modular, flexible, and maintainable.


Facade Pattern

The Facade pattern can be used to hide the complexity of the subsystem and make it easier to use by providing a higher-level interface.

Let's consider a scenario where you are building a home theater application that controls various components like a projector, audio system, and lighting. Without using the Facade pattern, your implementation might look like this:

Before Facade Pattern:

Create separate classes for Projector, AudioSystem, and Lighting:


public class Projector {
    public void on() {
        System.out.println("Projector is on.");
    }

    public void off() {
        System.out.println("Projector is off.");
    }
}

public class AudioSystem {
    public void play() {
        System.out.println("Audio system is playing.");
    }

    public void stop() {
        System.out.println("Audio system is stopped.");
    }
}

public class Lighting {
    public void dim() {
        System.out.println("Lights are dimmed.");
    }

    public void bright() {
        System.out.println("Lights are bright.");
    }
}

Use the separate classes in your client code:

javaCopy code
public class Client {
    public static void main(String[] args) {
        Projector projector = new Projector();
        AudioSystem audioSystem = new AudioSystem();
        Lighting lighting = new Lighting();

        // Starting the movie
        projector.on();
        audioSystem.play();
        lighting.dim();

        // Stopping the movie
        projector.off();
        audioSystem.stop();
        lighting.bright();
    }
}

In this implementation, the client code has to interact with multiple classes directly, making it more complex and harder to manage.

After Facade Pattern:

Using the Facade pattern, you can create a single interface that controls the subsystem components, making it easier to use and manage.

Create a HomeTheaterFacade class that controls the subsystem components:


public class HomeTheaterFacade {
    private Projector projector;
    private AudioSystem audioSystem;
    private Lighting lighting;

    public HomeTheaterFacade(Projector projector, AudioSystem audioSystem, Lighting lighting) {
        this.projector = projector;
        this.audioSystem = audioSystem;
        this.lighting = lighting;
    }

    public void startMovie() {
        projector.on();
        audioSystem.play();
        lighting.dim();
    }

    public void stopMovie() {
        projector.off();
        audioSystem.stop();
        lighting.bright();
    }
}

Use the HomeTheaterFacade in your client code:


public class Client {
    public static void main(String[] args) {
        Projector projector = new Projector();
        AudioSystem audioSystem = new AudioSystem();
        Lighting lighting = new Lighting();

        HomeTheaterFacade homeTheater = new HomeTheaterFacade(projector, audioSystem, lighting);

        // Starting the movie
        homeTheater.startMovie();

        // Stopping the movie
        homeTheater.stopMovie();
    }
}

Now, the client code interacts with a single interface, HomeTheaterFacade, which simplifies the interaction with the subsystem components. The Facade pattern has made the codebase more modular, easier to use, and maintainable.


Flyweight Pattern

For instance, in your application, you might have multiple similar objects in a list. Some properties within these objects can be the same as those in other objects, such as genre and publisher in a list of book objects. To optimize this, you can create a cache-like list to store the shared properties like genre and publisher, calling it BookMetaData. When you create the book list, you can then retrieve these shared properties from the BookMetaData cache.

Before Flyweight Pattern:

Create a Book class:


public class Book {
    private String id;
    private String title;
    private String author;
    private String publisher;
    private String genre;

    // Constructors, getters, and setters
}

After Flyweight Pattern:

Using the Flyweight pattern, you can separate the shared and unique state of the Book objects and minimize memory usage.

Create a BookMetadata class with shared state:


public class BookMetadata {
    private String publisher;
    private String genre;

    // Constructors, getters, and setters
}

Create a Book class that only contains the unique state:


public class Book {
    private String id;
    private String title;
    private String author;
    private BookMetadata metadata;

    // Constructors, getters, and setters
}

Create a BookMetadataFactory class to manage the shared state:


@Component
public class BookMetadataFactory {
    private Map<String, BookMetadata> metadataMap = new HashMap<>();

    public BookMetadata getBookMetadata(String publisher, String genre) {
        String key = publisher + "-" + genre;
        if (!metadataMap.containsKey(key)) {
            metadataMap.put(key, new BookMetadata(publisher, genre));
        }
        return metadataMap.get(key);
    }
}

Use the BookMetadataFactory, BookMetadata, and Book classes in your API:


@RestController
public class BookController {
    @Autowired
    private BookMetadataFactory bookMetadataFactory;

    @GetMapping("/books")
    public List<Book> getBooks() {
        // Fetch books from database or other data source
        // ...

        // Create Book objects using BookMetadataFactory
        List<Book> books = new ArrayList<>();
        for (BookData bookData : fetchedBookData) {
            BookMetadata metadata = bookMetadataFactory.getBookMetadata(bookData.getPublisher(), bookData.getGenre());
            Book book = new Book(bookData.getId(), bookData.getTitle(), bookData.getAuthor(), metadata);
            books.add(book);
        }

        return books;
    }
}

Now, the shared state of the Book objects is managed by the BookMetadataFactory, which minimizes memory usage by only creating a single instance of each unique combination of publisher and genre. The Flyweight pattern has made the codebase more memory-efficient and maintainable.


Behavioral patterns

Behavioral patterns focus on improving communication and interactions between objects, while structural patterns concentrate on creating maintainable and flexible relationships between objects to simplify the application's architecture.

Chain of responsibility

In the Chain of Responsibility pattern, "decoupled" refers to the separation between the sender of a request and its receivers. This separation ensures that the sender does not have direct knowledge of which object will handle the request, allowing for greater flexibility and modularity in the system. By decoupling these components, you can easily add, remove, or modify handlers in the chain without impacting the sender or other parts of the system.

Let's consider a scenario where you have a support system for a software company. There are different levels of support staff, such as Junior Support, Senior Support, and Manager. Each level is capable of handling a certain complexity of support issues. When a support ticket comes in, it should be handled by the appropriate support level.

Implementation:

Create a Support interface:

 
public interface Support {
    void setNext(Support nextSupport);
    void handleIssue(SupportIssue issue);
}

Create the SupportIssue class:

 
public class SupportIssue {
    private String description;
    private int complexity;

    public SupportIssue(String description, int complexity) {
        this.description = description;
        this.complexity = complexity;
    }

    public int getComplexity() {
        return complexity;
    }
}

Create concrete Support classes for each support level:

 
public class JuniorSupport implements Support {
    private Support nextSupport;

    @Override
    public void setNext(Support nextSupport) {
        this.nextSupport = nextSupport;
    }

    @Override
    public void handleIssue(SupportIssue issue) {
        if (issue.getComplexity() <= 5) {
            System.out.println("Junior Support handled the issue: " + issue.getComplexity());
        } else if (nextSupport != null) {
            nextSupport.handleIssue(issue);
        }
    }
}

public class SeniorSupport implements Support {
    private Support nextSupport;

    @Override
    public void setNext(Support nextSupport) {
        this.nextSupport = nextSupport;
    }

    @Override
    public void handleIssue(SupportIssue issue) {
        if (issue.getComplexity() > 5 && issue.getComplexity() <= 10) {
            System.out.println("Senior Support handled the issue: " + issue.getComplexity());
        } else if (nextSupport != null) {
            nextSupport.handleIssue(issue);
        }
    }
}

public class Manager implements Support {
    private Support nextSupport;

    @Override
    public void setNext(Support nextSupport) {
        this.nextSupport = nextSupport;
    }

    @Override
    public void handleIssue(SupportIssue issue) {
        if (issue.getComplexity() > 10) {
            System.out.println("Manager handled the issue: " + issue.getComplexity());
        } else if (nextSupport != null) {
            nextSupport.handleIssue(issue);
        }
    }
}

Use the Chain of Responsibility pattern in the client code:

 
public class Client {
    public static void main(String[] args) {
        Support juniorSupport = new JuniorSupport();
        Support seniorSupport = new SeniorSupport();
        Support manager = new Manager();

        // Set up the chain of responsibility
        juniorSupport.setNext(seniorSupport);
        seniorSupport.setNext(manager);

        // Create support issues with varying complexity levels
        SupportIssue issue1 = new SupportIssue("Issue 1", 3);
        SupportIssue issue2 = new SupportIssue("Issue 2", 7);
        SupportIssue issue3 = new SupportIssue("Issue 3", 12);

			juniorSupport.handleIssue(issue1);
        juniorSupport.handleIssue(issue2);
        juniorSupport.handleIssue(issue3);

In the main method of the Client class, we create instances of JuniorSupport, SeniorSupport, and Manager. Then we set up the chain of responsibility by linking the support levels using the setNext() method. In this example, the chain starts with JuniorSupport, followed by SeniorSupport, and finally, the Manager.

Next, we create three SupportIssue instances with different complexity levels (3, 7, and 12). We then pass these issues through the chain of responsibility by calling the handleIssue() method on the JuniorSupport instance.


Command Pattern

In the Command pattern, the term "decoupled" refers to the separation between the object that invokes an action (the invoker) and the object that performs the action (the receiver). This separation is achieved by encapsulating the request as a separate command object containing all necessary information. By decoupling these components, you can easily change, extend, or reuse the invoker, command, and receiver independently, enhancing the flexibility and modularity of the system.

Let's consider a scenario where you have a simple text editor. The text editor supports basic actions like typing, copying, pasting, and undoing. Using the Command pattern, you can create a command for each action and decouple the text editor's interface from the underlying actions.

Implementation:

Create a Command interface:

 
public interface Command {
    void execute();
}

Create concrete Command classes for each action:


public class TypeCommand implements Command {
    private TextEditor editor;
    private char character;

    public TypeCommand(TextEditor editor, char character) {
        this.editor = editor;
        this.character = character;
    }

    @Override
    public void execute() {
        editor.type(character);
    }
}

public class CopyCommand implements Command {
    private TextEditor editor;

    public CopyCommand(TextEditor editor) {
        this.editor = editor;
    }

    @Override
    public void execute() {
        editor.copy();
    }
}

public class PasteCommand implements Command {
    private TextEditor editor;

    public PasteCommand(TextEditor editor) {
        this.editor = editor;
    }

    @Override
    public void execute() {
        editor.paste();
    }
}

public class UndoCommand implements Command {
    private TextEditor editor;

    public UndoCommand(TextEditor editor) {
        this.editor = editor;
    }

    @Override
    public void execute() {
        editor.undo();
    }
}

Create the TextEditor class that performs the actual actions:

 
public class TextEditor {
    private StringBuilder text = new StringBuilder();

    public void type(char character) {
        text.append(character);
    }

    public void copy() {
        // Perform the copy action
    }

    public void paste() {
        // Perform the paste action
    }

    public void undo() {
        // Perform the undo action
    }

    public String getText() {
        return text.toString();
    }
}

Use the Command pattern in the client code:

 
public class Client {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();

        // Create command instances
        Command typeA = new TypeCommand(editor, 'A');
        Command typeB = new TypeCommand(editor, 'B');
        Command typeC = new TypeCommand(editor, 'C');
        Command copy = new CopyCommand(editor);
        Command paste = new PasteCommand(editor);
        Command undo = new UndoCommand(editor);

        // Execute commands
        typeA.execute();
        typeB.execute();
        typeC.execute();
        copy.execute();
        paste.execute();
        undo.execute();

        System.out.println("Editor content: " + editor.getText());
    }
}

In this example, we create the TextEditor class to perform the actual actions (typing, copying, pasting, and undoing). The Command interface and its concrete implementations encapsulate the actions as separate command objects. This allows the client code to create and execute commands without directly invoking the methods on the TextEditor object, decoupling the sender of the request from the receiver.

By decoupling the sender and receiver, you can:

  1. Change the receiver's implementation without affecting the sender.
  2. Reuse the sender with different receivers or commands.
  3. Add new commands without modifying the sender or receiver.
  4. Store, queue, or log commands before executing them.

Mediator pattern

It that promotes loose coupling between objects by centralizing communication between them through a mediator object. The mediator coordinates the interactions between objects, allowing them to interact without direct references to each other. This pattern is useful when you have a complex web of objects that need to communicate with each other and you want to simplify their relationships and reduce coupling.

Let's consider an example where we have a chat room where multiple users can send and receive messages.

A real-world example of the Mediator pattern is the Air Traffic Control (ATC) system at airports. The ATC system acts as a mediator between different airplanes, ensuring that they can communicate and coordinate their actions without directly interacting with each other.

Implementation:

Create a Mediator interface:


public interface ATCMediator {
    void sendMessage(Airplane airplane, String message);
    void addAirplane(Airplane airplane);
}

Create a concrete Mediator class:


public class ATCControlTower implements ATCMediator {
    private List<Airplane> airplanes;

    public ATCControlTower() {
        this.airplanes = new ArrayList<>();
    }

    @Override
    public void addAirplane(Airplane airplane) {
        airplanes.add(airplane);
    }

    @Override
    public void sendMessage(Airplane sender, String message) {
        for (Airplane airplane : airplanes) {
            if (airplane != sender) {
                airplane.receiveMessage(sender.getCallSign(), message);
            }
        }
    }
}

Create an Airplane class:

 
public class Airplane {
    private String callSign;
    private ATCMediator mediator;

    public Airplane(String callSign, ATCMediator mediator) {
        this.callSign = callSign;
        this.mediator = mediator;
    }

    public String getCallSign() {
        return callSign;
    }

    public void sendMessage(String message) {
        mediator.sendMessage(this, message);
    }

    public void receiveMessage(String sender, String message) {
        System.out.println(sender + " -> " + callSign + ": " + message);
    }
}

Use the Mediator pattern in the client code:

 
public class Client {
    public static void main(String[] args) {
        ATCMediator controlTower = new ATCControlTower();

        Airplane airplane1 = new Airplane("APL123", controlTower);
        Airplane airplane2 = new Airplane("BPL456", controlTower);
        Airplane airplane3 = new Airplane("CPL789", controlTower);

        controlTower.addAirplane(airplane1);
        controlTower.addAirplane(airplane2);
        controlTower.addAirplane(airplane3);

        airplane1.sendMessage("Requesting permission to land.");
        airplane2.sendMessage("Requesting permission to take off.");
        airplane3.sendMessage("Requesting permission to taxi.");
    }
}

In this example, we create an ATCMediator interface with sendMessage and addAirplane methods, and a concrete ATCControlTower class that implements this interface. The ATCControlTower class maintains a list of airplanes and defines how messages are sent and received between them.

The Airplane class represents airplanes in the airspace. It has a reference to the mediator and uses it to send messages. When an airplane receives a message, it prints the message to the console.

The client code creates an ATCControlTower mediator and several Airplane instances, adds the airplanes to the control tower, and simulates message exchanges between them.


Null Object Pattern

that uses a special object (null object) to represent a default behavior when a given object is expected to be null. The pattern aims to avoid null references and the need for conditional statements to check for null values.

Let's consider an example where we have a list of customers, and we want to perform an action on the customers without encountering null references.

Implementation:

Create an AbstractCustomer class:

 
public abstract class AbstractCustomer {
    protected String name;

    public abstract String getName();
    public abstract boolean isNull();
}

Create concrete Customer and NullCustomer classes:

 
public class Customer extends AbstractCustomer {

    public Customer(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public boolean isNull() {
        return false;
    }
}

public class NullCustomer extends AbstractCustomer {

    @Override
    public String getName() {
        return "Not Available";
    }

    @Override
    public boolean isNull() {
        return true;
    }
}

Create a CustomerFactory class:

 
public class CustomerFactory {
    private List<String> customerNames;

    public CustomerFactory() {
        customerNames = new ArrayList<>();
        customerNames.add("Alice");
        customerNames.add("Bob");
        customerNames.add("Charlie");
    }

    public AbstractCustomer getCustomer(String name) {
        if (customerNames.contains(name)) {
            return new Customer(name);
        }
        return new NullCustomer();
    }
}

Use the Null Object pattern in the client code:

 
public class Client {
    public static void main(String[] args) {
        CustomerFactory customerFactory = new CustomerFactory();

        AbstractCustomer customer1 = customerFactory.getCustomer("Alice");
        AbstractCustomer customer2 = customerFactory.getCustomer("Bob");
        AbstractCustomer customer3 = customerFactory.getCustomer("Charlie");
        AbstractCustomer customer4 = customerFactory.getCustomer("Dave");

        System.out.println("Customers:");
        System.out.println(customer1.getName());
        System.out.println(customer2.getName());
        System.out.println(customer3.getName());
        System.out.println(customer4.getName());
    }
}

In this example, we create an AbstractCustomer class with getName() and isNull() methods. The Customer class represents a concrete customer with a name, while the NullCustomer class represents the null object with default behavior.

The CustomerFactory class is responsible for creating and returning customer objects based on the given name. If the customer name is found in the list, it returns a Customer object with the name; otherwise, it returns a NullCustomer object.

The client code uses the CustomerFactory to obtain customer objects and perform actions on them without encountering null references. The output will look like this:

makefileCopy code
Customers:
Alice
Bob
Charlie
Not Available

The Null Object pattern ensures that you don't encounter null references in your code and allows you to handle missing objects with a default behavior, simplifying your code by removing the need for null checks.


Observer Pattern

The Observer pattern is a behavioral design pattern that defines a one-to-many dependency between objects so that when one object (the subject) changes state, all its dependents (observers) are notified and updated automatically. This pattern is useful when you need to maintain consistency between related objects without making them tightly coupled.

A real-world example of the Observer pattern is a weather monitoring system where multiple weather display devices need to be updated when new weather data is available.

Implementation:

Create a Subject interface:

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

Create an Observer interface:

 
public interface Observer {
    void update(float temperature, float humidity, float pressure);
}

Create a WeatherData class implementing the Subject interface:

 
public class WeatherData implements Subject {
    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() {
        observers = new ArrayList<>();
    }

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

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

    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(temperature, humidity, pressure);
        }
    }

    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        notifyObservers();
    }
}

Create a CurrentConditionsDisplay class implementing the Observer interface:

 
public class CurrentConditionsDisplay implements Observer {
    private float temperature;
    private float humidity;
    private Subject weatherData;

    public CurrentConditionsDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }

    public void display() {
        System.out.println("Current conditions: " + temperature + "°C and " + humidity + "% humidity");
    }
}

Use the Observer pattern in the client code:

 
public class Client {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();

        CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);

        weatherData.setMeasurements(28.5f, 65f, 1010f);
        weatherData.setMeasurements(30.0f, 70f, 1012f);
    }
}

In this example, we create a Subject interface with methods to register, remove, and notify observers, and an Observer interface with an update method to receive updates from the subject. The WeatherData class implements the Subject interface, maintaining a list of observers and providing methods to manipulate weather data. The CurrentConditionsDisplay class implements the Observer interface and displays the current weather conditions.

The client code creates a WeatherData subject and a CurrentConditionsDisplay observer, and simulates updates to the weather data. The output will look like this:


Current conditions: 28.5°C and 65.0% humidity
Current conditions: 30.0°C and 70.0% humidity

State Pattern

The State pattern is a behavioral design pattern that allows an object to change its behavior when its internal state changes. Instead of implementing all the possible states and their behavior within one class, this pattern separates the states into separate classes, which makes the code more maintainable and easier to understand.

Here's a simple example of a document approval process using the State pattern in Java:

Create a DocumentState interface:

 
public interface DocumentState {
    void submit(Document document);
    void approve(Document document);
    void reject(Document document);
}

Create concrete state classes:

 
public class DraftState implements DocumentState {
    @Override
    public void submit(Document document) {
        System.out.println("Document submitted for approval.");
        document.setState(new SubmittedState());
    }

    @Override
    public void approve(Document document) {
        System.out.println("Document is still in draft state. Submit the document first.");
    }

    @Override
    public void reject(Document document) {
        System.out.println("Document is still in draft state. Submit the document first.");
    }
}

public class SubmittedState implements DocumentState {
    @Override
    public void submit(Document document) {
        System.out.println("Document already submitted for approval.");
    }

    @Override
    public void approve(Document document) {
        System.out.println("Document approved.");
        document.setState(new ApprovedState());
    }

    @Override
    public void reject(Document document) {
        System.out.println("Document rejected.");
        document.setState(new RejectedState());
    }
}

public class ApprovedState implements DocumentState {
    @Override
    public void submit(Document document) {
        System.out.println("Document already approved.");
    }

    @Override
    public void approve(Document document) {
        System.out.println("Document already approved.");
    }

    @Override
    public void reject(Document document) {
        System.out.println("Document already approved. Cannot reject.");
    }
}

public class RejectedState implements DocumentState {
    @Override
    public void submit(Document document) {
        System.out.println("Document resubmitted for approval.");
        document.setState(new SubmittedState());
    }

    @Override
    public void approve(Document document) {
        System.out.println("Document is in rejected state. Submit the document first.");
    }

    @Override
    public void reject(Document document) {
        System.out.println("Document already rejected.");
    }
}

Create the Document class:

 
public class Document {
    private DocumentState state;

    public Document() {
        state = new DraftState();
    }

    public void setState(DocumentState state) {
        this.state = state;
    }

    public void submit() {
        state.submit(this);
    }

    public void approve() {
        state.approve(this);
    }

    public void reject() {
        state.reject(this);
    }
}

Use the State pattern in the client code:

 
public class Client {
    public static void main(String[] args) {
        Document document = new Document();

        document.submit();
        document.approve();

        document.submit(); // Should not be allowed, as the document is already approved.
        document.reject(); // Should not be allowed, as the document is already approved.
    }
}

In this example, we create a DocumentState interface with methods for submitting, approving, and rejecting a document. We then create concrete state classes like DraftState, SubmittedState, ApprovedState, and RejectedState, which implement the DocumentState interface and define the behavior for each state.

The Document class has a reference to the DocumentState interface and delegates the actions to the concrete state classes. It also provides methods for changing the current state.

The client code creates a Document instance and performs actions like submitting, approving, and rejecting the document. The Document instance changes its state based on the performed actions, and the behavior is determined by the concrete state classes.


Strategy Pattern

The Strategy pattern allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. The pattern lets the algorithm vary independently from the clients that use it.

In other words, the Strategy pattern enables you to switch the algorithm or logic being used in an object at runtime, depending on the situation or configuration.

Real-world example:

Let's consider a simple example of a navigation application that calculates the best route between two locations. The application can use different strategies for finding the best route, such as the shortest distance, the fastest route, or the most scenic route. Users can choose the strategy they prefer for a particular trip.

Implementation:

  1. Create a RouteStrategy interface:
 
public interface RouteStrategy {
    Route calculateRoute(Location start, Location end);
}

Create concrete strategy classes:

 
public class ShortestDistanceStrategy implements RouteStrategy {
    @Override
    public Route calculateRoute(Location start, Location end) {
        // Calculate the shortest distance route between start and end
    }
}

public class FastestRouteStrategy implements RouteStrategy {
    @Override
    public Route calculateRoute(Location start, Location end) {
        // Calculate the fastest route between start and end
    }
}

public class ScenicRouteStrategy implements RouteStrategy {
    @Override
    public Route calculateRoute(Location start, Location end) {
        // Calculate the most scenic route between start and end
    }
}

Create the Navigation class:

 
public class Navigation {
    private RouteStrategy routeStrategy;

    public Navigation(RouteStrategy routeStrategy) {
        this.routeStrategy = routeStrategy;
    }

    public void setRouteStrategy(RouteStrategy routeStrategy) {
        this.routeStrategy = routeStrategy;
    }

    public Route calculateRoute(Location start, Location end) {
        return routeStrategy.calculateRoute(start, end);
    }
}

Use the Strategy pattern in the client code:

 
public class Client {
    public static void main(String[] args) {
        Navigation navigation = new Navigation(new ShortestDistanceStrategy());
        Route shortestRoute = navigation.calculateRoute(startLocation, endLocation);

        navigation.setRouteStrategy(new FastestRouteStrategy());
        Route fastestRoute = navigation.calculateRoute(startLocation, endLocation);

        navigation.setRouteStrategy(new ScenicRouteStrategy());
        Route scenicRoute = navigation.calculateRoute(startLocation, endLocation);
    }
}

In this example, we create a RouteStrategy interface with a method for calculating the route between two locations. We then create concrete strategy classes like ShortestDistanceStrategy, FastestRouteStrategy, and ScenicRouteStrategy, which implement the RouteStrategy interface and define the behavior for each strategy.

The Navigation class has a reference to the RouteStrategy interface and delegates the route calculation to the concrete strategy classes. It also provides a method for changing the current strategy.

The client code creates a Navigation instance and uses it to calculate routes based on different strategies. The Navigation instance changes its strategy based on the user's preference, and the behavior is determined by the concrete strategy classes.


Template Pattern

The Template Method pattern  that defines the skeleton of an algorithm in a base class but allows subclasses to override specific steps of the algorithm without changing its overall structure. This pattern enables you to provide a consistent structure for an algorithm while allowing customization of specific steps by the subclasses.

The Template Method pattern is useful when you have a series of steps that must be performed in a particular order, but some of the steps can have different implementations or can be optional depending on the context.

Example:

Let's consider a simple example of a data processing system that reads, processes, and saves data. The steps to read, process, and save the data are fixed, but the specific way of reading, processing, or saving can be different based on the data format or storage system.

Implementation:

Create an abstract base class with the template method and abstract methods for the customizable steps:

 
public abstract class DataProcessor {

    // Template method
    public final void processData() {
        Data data = readData();
        Data processedData = process(data);
        saveData(processedData);
    }

    // Abstract methods for customizable steps
    protected abstract Data readData();
    protected abstract Data process(Data data);
    protected abstract void saveData(Data processedData);
}

Create concrete subclasses for different data processing scenarios:

 
public class CsvDataProcessor extends DataProcessor {

    @Override
    protected Data readData() {
        // Read data from a CSV file
    }

    @Override
    protected Data process(Data data) {
        // Process data specific to CSV format
    }

    @Override
    protected void saveData(Data processedData) {
        // Save processed data to a file or database
    }
}

public class JsonDataProcessor extends DataProcessor {

    @Override
    protected Data readData() {
        // Read data from a JSON file
    }

    @Override
    protected Data process(Data data) {
        // Process data specific to JSON format
    }

    @Override
    protected void saveData(Data processedData) {
        // Save processed data to a file or database
    }
}

In this example, the DataProcessor abstract base class defines the template method processData(), which outlines the structure of the data processing algorithm. The readData(), process(), and saveData() methods are abstract, allowing subclasses to provide their own implementations for these steps.

The CsvDataProcessor and JsonDataProcessor subclasses implement the abstract methods to provide specific behavior for reading, processing, and saving data in CSV and JSON formats, respectively.

By using the Template Method pattern, you can ensure that the overall structure of the data processing algorithm remains consistent, while allowing for customization and flexibility in the specific steps of the algorithm.


Visitor Pattern

The Visitor pattern  allows you to separate the algorithm from the objects on which it operates. It enables you to define new operations without modifying the classes of the elements that you work with.

The Visitor pattern is useful when you have a complex object structure, like an object tree, and you need to perform operations on the objects without changing their classes.

Example:

Let's consider a simple example of a tax calculator for a shopping cart that has different types of items, such as books and electronics. The tax calculation can vary based on the type of item.

Implementation:

Create an interface for the item elements:

 
public interface ItemElement {
    double accept(TaxVisitor visitor);
}

Create concrete item classes:

 
public class Book implements ItemElement {
    private double price;

    public Book(double price) {
        this.price = price;
    }

    public double getPrice() {
        return price;
    }

    @Override
    public double accept(TaxVisitor visitor) {
        return visitor.visit(this);
    }
}

public class Electronic implements ItemElement {
    private double price;

    public Electronic(double price) {
        this.price = price;
    }

    public double getPrice() {
        return price;
    }

    @Override
    public double accept(TaxVisitor visitor) {
        return visitor.visit(this);
    }
}

Create the TaxVisitor interface:

 
public interface TaxVisitor {
    double visit(Book book);
    double visit(Electronic electronic);
}

Create a concrete visitor class:

 
public class TaxCalculator implements TaxVisitor {

    @Override
    public double visit(Book book) {
        // Calculate tax for the book
        return book.getPrice() * 0.1;
    }

    @Override
    public double visit(Electronic electronic) {
        // Calculate tax for the electronic item
        return electronic.getPrice() * 0.2;
    }
}

Use the Visitor pattern in the client code:

 
public class Client {
    public static void main(String[] args) {
        ItemElement[] items = new ItemElement[]{
                new Book(50),
                new Electronic(100)
        };

        TaxVisitor taxCalculator = new TaxCalculator();
        double totalTax = 0;

        for (ItemElement item : items) {
            totalTax += item.accept(taxCalculator);
        }

        System.out.println("Total tax: " + totalTax);
    }
}

In this example, the ItemElement interface declares an accept() method that takes a TaxVisitor as an argument. Concrete item classes like Book and Electronic implement the ItemElement interface and define the accept() method.

The TaxVisitor interface declares visit() methods for each concrete item class. The TaxCalculator class implements the TaxVisitor interface and provides the tax calculation logic for each item type.

The client code creates an array of ItemElement objects, representing the shopping cart items. It then iterates through the items and calls the accept() method on each item, passing the TaxCalculator instance as the visitor. This results in the appropriate visit() method being called for each item type, calculating the tax for each item.

By using the Visitor pattern, you can separate the tax calculation logic from the item classes and easily add new item types or change the tax calculation logic without modifying the existing item classes.

The name "Visitor" in the Visitor pattern comes from the metaphor of a visitor who goes to a place or object and performs some operations or actions on that object. In this pattern, the Visitor object "visits" the elements of a structure or a set of objects, performing specific operations on them.

The reason for this name is that the pattern enables you to separate the algorithm or behavior from the objects on which it operates. The Visitor object carries the behavior or the algorithm and is responsible for applying it to the elements it visits. The elements themselves provide a way to accept the Visitor and let it perform the required actions on them.

This separation of concerns allows you to easily add new operations or behaviors to the object structure without modifying the existing classes of the elements. Thus, the name "Visitor" reflects the core idea of this pattern - an external object visiting and performing actions on the elements of a structure.