Skip to content

Instantly share code, notes, and snippets.

@AaradhyaSaxena
Last active April 5, 2024 00:32
Show Gist options
  • Save AaradhyaSaxena/1f9de33126f3a5b0ad20602a4a4157b3 to your computer and use it in GitHub Desktop.
Save AaradhyaSaxena/1f9de33126f3a5b0ad20602a4a4157b3 to your computer and use it in GitHub Desktop.
Design Patterns

Design Patterns

Design patterns can be classified into three main categories:

Creational Patterns:

Creational design patterns relate to how objects are constructed from classes. Examples of creational patterns include Factory Method, Abstract Factory, Builder, Prototype, and Singleton.

Structural Patterns:

Structural patterns are concerned with the composition of classes i.e. how the classes are made up or constructed. Examples of structural patterns include Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy.

Behavioral Patterns:

Behavioral design patterns dictate the interaction of classes and objects amongst eachother and the delegation of responsibility. Examples of behavioral patterns include Observer, Strategy, Template Method, Command, Iterator, State, Visitor, and Chain of Responsibility.

OOD Examples

Parkinglot

  • The ParkingLot class will use the Singleton design pattern, because there will only be a single instance of the parking lot system.
  • This parking lot system is also composed of smaller objects that we have already designed, like entrance, exit, parking spots, parking rates, etc. Therefore, it will be a good practice to use the Abstract Factory and Factory design pattern to instantiate all those objects.

Elevator System

  • The Strategy design pattern can be applied here since the system could have multiple dispatch request strategy classes. Therefore, depending on the particular layout of the building and its scenarios, we choose a set of dispatch request strategy classes.
  • We can also use the State and Delegation design pattern for this problem. Instead of implementing all methods on its own, the context object stores a reference to one of the state objects that represents its current state and delegates all the state-specific tasks to that object. For example, elevators have multiple states like working or idle, etc. Based on the state, the system infers which method or behavior of the elevator should be invoked.

Library Management System

  • We can apply the Factory design pattern to create objects and mandate that they go via a single factory. For example, we can create a BookFactory class to create a book object in an arranged manner.
  • Similarly, we can use the Delegation design pattern to delegate a task from one class to another class. For example, librarian functionalities like adding book items, deleting book items, or modifying book items are actually implemented in the BookItem class. The Librarian class uses the BookItem class and has access to its data and methods.
  • Moreover, we can use the Observer design pattern to notify library members. For example, if a member searches for a book that is unavailable at that time, then the observer interface system will notify the member when that book is available for reservation.

Car Rental

  • Discount decorator: It can be used to apply discounts to all types of vehicles in our car rental system.
  • Peak season decorator: It can be used to increase the fare of all types of vehicles in our car rental system.
  • Damage fine decorator: When the vehicle is returned, this decorator can help in calculating the fine due to car damage.
  • Partially filled fuel tank fine decorator: When the vehicle is returned, this decorator can help calculate the fine due to the partially filled fuel tank.

Vending Machine

  • We have used the State design pattern to design this problem because, in different states, we perform different or specific tasks according to the state. The vending machine changes its behavior based on its state. The different states within the system are listed below:
    • No money inserted state
    • Money inserted state
    • Dispense state

ATM

  • The Singleton design pattern: This pattern ensures the existence of a single instance of the ATM at a given moment that can be accessed by multiple users, due to the shared nature of the ATM components.
  • The State design pattern: This pattern enables the ATM to alter its behavior based on the internal changes in the machine. This way, an ATM can transition from one state to another, like switching from an idle state to displaying an account balance or money withdrawal state, and as soon as all the operations have been performed, it can switch back to the initial idle state.
  • The following design patterns can also be used to design ATM:
    • The Composite design pattern can be used to combine different components of the ATM along with their functionalities.
    • The Builder design pattern allows the same processes for a complex object to have different representations. In the ATM system, it can help separate different kinds of transactions like withdrawals, deposits, etc.

Factory Pattern

The Factory Pattern is a creational design pattern that provides a way to create objects without exposing the instantiation logic to the client code. It encapsulates the object creation process in a separate class or method called the "factory" and allows clients to create objects through an interface or a base class without knowing the concrete implementation details.

Eg: response mapper - ajio, jiomart

# Abstract Product
class Mapper(ABC):
    @abstractmethod
    def map_data(self, data):
        pass

# Concrete Products
class JSONMapper(Mapper):
    def map_data(self, data):
        # Logic to map data to JSON format
        return f"JSON: {data}"

class XMLMapper(Mapper):
    def map_data(self, data):
        # Logic to map data to XML format
        return f"XML: {data}"

# Factory
class MapperFactory:
    def create_mapper(self, format_type):
        if format_type.lower() == 'json':
            return JSONMapper()
        elif format_type.lower() == 'xml':
            return XMLMapper()
        else:
            raise ValueError("Unsupported format type")

# Client Code
def use_mapper(data, format_type):
    factory = MapperFactory()
    mapper = factory.create_mapper(format_type)
    result = mapper.map_data(data)
    return result

# Usage
json_result = use_mapper({"key": "value"}, "json")
xml_result = use_mapper({"key": "value"}, "xml")

print(json_result)  # Output: JSON: {'key': 'value'}
print(xml_result)   # Output: XML: {'key': 'value'}

Singleton Pattern

The Singleton Pattern is a creational design pattern that ensures the existence of only one instance of a class and provides a global point of access to that instance. It is commonly used when you want to restrict the instantiation of a class to a single object throughout the application.

public class Singleton {
    private static Singleton instance;
    
    // Private constructor to prevent direct instantiation
    private Singleton() {
    }
    
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
    
    public void doSomething() {
        // Perform some operation
    }
}

In the above example, the Singleton class has a private constructor to prevent direct instantiation from outside the class. The getInstance() method is used to get the single instance of the Singleton class. If the instance doesn't exist, it creates a new instance; otherwise, it returns the existing instance.

There are variations of the Singleton Pattern that handle thread-safety, eager initialization, or serialization concerns. These variations include using synchronized methods or blocks, using the "double-checked locking" approach, or implementing the Singleton as an enumeration.

Eg: object mapper class

Other usescases:

  1. Useful in managing resources such as database connections or file systems. By using a single instance of a connection pool, you ensure that your application doesn't overload the database with multiple connections, which can be costly in terms of performance and resource utilization. Eg: ConnectionPool class.
  2. Singletons can manage access to shared resources, such as a logging utility, cache, or print spooler. This ensures coordinated access and can improve the application's overall efficiency by avoiding redundant instances or conflicting accesses. If configuration settings are allowed to change during runtime and multiple instances are used, some instances may have outdated configuration data. Example: Two instances of a ConfigLoader class are used in different parts of the application. If one instance updates a setting, the other instance remains unaware of this change, leading to inconsistent behavior.
  3. If you have a class responsible for reading configuration settings from a file and you instantiate this class multiple times, each instance may read the file once, increasing I/O operations unnecessarily. Imagine a ConfigLoader class that loads configuration settings from a file into a hashMap then with multiple instances, the configuration file is read multiple times, and stored in multiple maps.

Prototype Pattern

The Prototype Pattern is a creational design pattern that focuses on creating objects by cloning or copying existing instances. Instead of creating objects from scratch, the Prototype Pattern provides a mechanism to create new instances by copying the properties and state of existing objects, known as prototypes.

import java.util.HashMap;
import java.util.Map;

// Prototype interface
interface Prototype {
    Prototype clone();
    void setProperty(String property);
    void printInfo();
}

// Concrete prototype class
class ConcretePrototype implements Prototype {
    private String property;

    @Override
    public Prototype clone() {
        ConcretePrototype clone = new ConcretePrototype();
        clone.setProperty(this.property);
        return clone;
    }

    @Override
    public void setProperty(String property) {
        this.property = property;
    }

    @Override
    public void printInfo() {
        System.out.println("Property: " + property);
    }
}

// Prototype registry or manager
class PrototypeRegistry {
    private Map<String, Prototype> prototypes;

    public PrototypeRegistry() {
        prototypes = new HashMap<>();
    }

    public void registerPrototype(String key, Prototype prototype) {
        prototypes.put(key, prototype);
    }

    public Prototype createPrototype(String key) {
        Prototype prototype = prototypes.get(key);
        if (prototype != null) {
            return prototype.clone();
        }
        return null;
    }
}

// Client code
public class Main {
    public static void main(String[] args) {
        PrototypeRegistry registry = new PrototypeRegistry();

        ConcretePrototype prototype1 = new ConcretePrototype();
        prototype1.setProperty("Prototype 1");
        registry.registerPrototype("prototype1", prototype1);

        ConcretePrototype prototype2 = new ConcretePrototype();
        prototype2.setProperty("Prototype 2");
        registry.registerPrototype("prototype2", prototype2);

        Prototype clonedPrototype1 = registry.createPrototype("prototype1");
        if (clonedPrototype1 != null) {
            clonedPrototype1.printInfo(); // Output: Property: Prototype 1
        }

        Prototype clonedPrototype2 = registry.createPrototype("prototype2");
        if (clonedPrototype2 != null) {
            clonedPrototype2.printInfo(); // Output: Property: Prototype 2
        }
    }
}

In the Main class, we register two concrete prototypes (prototype1 and prototype2) in the prototype registry. We then create clones of the prototypes by requesting them from the registry using their keys. The cloned prototypes are created by performing a clone operation on the registered prototypes, ensuring that the new instances have the same properties as the original prototypes.

The Prototype Pattern allows you to create new objects by copying existing instances, avoiding the need to explicitly define new objects from scratch. It promotes flexibility and modularity by separating object creation from the client code and allowing easy creation of new instances with different properties. This pattern is useful when object creation is expensive or complex, and creating new instances by cloning existing ones provides a more efficient solution.

Eg: TicTacToe game - To undo a move OR run simulations

  • We used prototype pattern, made a copy of original board, tried running different moves on the copy to check for best move.
  • When making a copy make sure there is no issue due to copy by reference.

Example use-cases:

  1. Real-time gaming applications where objects like bullets, trees, or enemies need to be created rapidly and in large numbers. Cloning existing objects can be significantly faster than instantiating new ones, especially when the objects have intricate state configurations.
  2. If creating a new instance of an object requires exposing internal state details that should remain hidden, using cloning to create a new instance can preserve the encapsulation of the object's internal state.

Proxy Patterns

The Proxy Pattern is a structural design pattern that allows for the creation of an intermediary object, called a "proxy," which controls the access to another object, known as the "real subject." The proxy acts as a surrogate for the real subject and provides additional functionality or control over its behavior.

Let's consider a scenario where you have a Image interface representing an image and a RealImage class implementing that interface, which loads and displays the actual image file from disk. The Proxy Pattern can be used to create a ProxyImage class that acts as a placeholder for the real image and provides additional functionality such as lazy loading and access control.

// Image interface
public interface Image {
    void display();
}

// RealImage class implementing Image interface
public class RealImage implements Image {
    private String filename;

    public RealImage(String filename) {
        this.filename = filename;
        loadFromDisk();
    }

    private void loadFromDisk() {
        System.out.println("Loading image: " + filename);
        // Load image from disk
    }

    @Override
    public void display() {
        System.out.println("Displaying image: " + filename);
        // Display the image
    }
}

// ProxyImage class acting as a proxy for RealImage
public class ProxyImage implements Image {
    private String filename;
    private RealImage realImage;

    public ProxyImage(String filename) {
        this.filename = filename;
    }

    @Override
    public void display() {
        if (realImage == null) {
            realImage = new RealImage(filename);
        }
        realImage.display();
    }
}
Image image = new ProxyImage("image.jpg");

// The real image is not loaded yet
image.display();

// The real image is loaded and displayed
image.display();

The key aspect here is that the ProxyImage class controls access to RealImage by **delaying** its instantiation until the display method is called. This is a typical use case of the Proxy design pattern, where the proxy controls the creation and access of the object it's proxying, which in this case is RealImage.

The Proxy Pattern provides a level of indirection and control over access to the real subject. It can be useful in various scenarios, such as lazy loading of resources, access control, caching, logging, or providing a simplified interface to a complex object. The client code interacts with the proxy object, unaware of the underlying real subject and the additional functionality provided by the proxy.

Use-cases:

  1. Access Control: Description: The proxy can control access to the real subject, allowing certain operations while denying others based on predefined criteria. Real-World Example: An internet proxy server controls access to websites based on company policies. Employees might be allowed to access educational sites but not social media platforms during work hours.
  2. Lazy Initialization: Description: The proxy can delay the creation and initialization of expensive objects until the moment they are actually needed. Real-World Example: In an image viewer application, thumbnails are loaded initially, and full-size images (real subjects) are only loaded (possibly from a remote server) when the user decides to view them in detail.
  3. Caching: Description: The proxy can cache results of operations or data from the real subject and serve repeated requests without involving the real subject again, thus improving performance. Real-World Example: A web proxy cache stores copies of web pages. When a user requests a page, the proxy serves the cached version if available, reducing bandwidth usage and load times.
  4. Logging and Monitoring: Description: The proxy can keep track of the operations performed on the real subject, logging usage statistics or monitoring for unusual behavior. Real-World Example: A database proxy logs all queries to monitor performance or audit access for security purposes, without altering the database's core functionality. We can have a wrapper class for Logging which implements say DB class. Similarly: Protection Proxy: Add wrapper for security related stuff.
public class LoggingDatabaseProxy implements Database {
    private final Database realDatabase;

    public LoggingDatabaseProxy(Database realDatabase) {
        this.realDatabase = realDatabase;
    }
    @Override
    public void executeQuery(String query) {
        System.out.println("LoggingDatabaseProxy: Logging query: " + query);
        long startTime = System.currentTimeMillis();
        
        realDatabase.executeQuery(query);  // Forward the request to the real database
        
        long endTime = System.currentTimeMillis();
        System.out.println("LoggingDatabaseProxy: Query executed in " + (endTime - startTime) + "ms");
    }
}
  1. Virtual Proxy: Description: This type of proxy creates expensive objects on demand. It's similar to lazy initialization but specifically focuses on deferring the creation of a resource-intensive object until it's needed. Real-World Example: In a graphics editing application, a virtual proxy might represent a large graphic object that is only fully loaded into memory when it's required for rendering.
  2. Remote Proxy: Description: This proxy represents an object that resides in a different address space or on a different machine, handling the intricacies of communicating with the object remotely. Real-World Example: A remote proxy could represent a service running on a cloud server. Clients interact with the proxy as if it were local, but the proxy handles all the details of remote communication.
  3. Smart Reference: Description: The proxy acts as a replacement for a bare pointer and performs additional actions when an object is accessed. Real-World Example: A reference counting proxy could keep track of how many clients are using an object and automatically release the object's resources when there are no more references.

By choosing the appropriate type of proxy, developers can add these layers of control without modifying the real subjects or the clients, adhering to the Open/Closed principle and promoting cleaner, more modular designs. It helps to promote loose coupling, separation of concerns, and code reusability by allowing you to introduce additional behavior through the proxy while keeping the client code unaware of the underlying changes.

Adaptor Pattern

The Adapter Pattern is a structural design pattern that allows incompatible interfaces to work together. It acts as a bridge between the interface of one class and the interface expected by the client, making it possible for the two interfaces to interact seamlessly.

Let's consider an example of an Adapter Pattern to convert a USB-C port into a standard USB-A port.

// Algolia Business Object (BO) Interface
public interface AlgoliaBO {
    String retrieveData();
}

// JioMart Data Transfer Object (DTO) Interface
public interface JioMartDTO {
    void sendData(String data);
}

// Adapter to convert AlgoliaBO to JioMartDTO
public class AlgoliaBO_to_JioMartDTO_Adapter implements JioMartDTO {
    private AlgoliaBO algoliaBO;

    public AlgoliaBO_to_JioMartDTO_Adapter(AlgoliaBO algoliaBO) {
        this.algoliaBO = algoliaBO;
    }

    @Override
    public void sendData(String data) {
        // Convert JioMartDTO method call to AlgoliaBO method call
        String retrievedData = algoliaBO.retrieveData();
        System.out.println("Transferring data via JioMartDTO: " + retrievedData);
    }
}

// Algolia Business Object
public class AlgoliaBOImpl implements AlgoliaBO {
    @Override
    public String retrieveData() {
        return "Data retrieved from Algolia BO";
    }
}

// Client Code
public class Client {
    public static void main(String[] args) {
        // Using Algolia BO as if it were a JioMart DTO using the adapter
        AlgoliaBO algoliaBO = new AlgoliaBOImpl();
        JioMartDTO jioMartDTO = new AlgoliaBO_to_JioMartDTO_Adapter(algoliaBO);

        // Client interacts with the JioMart DTO interface, but it's using Algolia BO behind the scenes.
        jioMartDTO.sendData("Hello, JioMart DTO!");
    }
}

Bridge pattern

The Bridge Pattern is a structural design pattern that decouples an abstraction from its implementation, allowing both to vary independently. It is useful when you have two hierarchies of classes with related functionality, and you want to avoid the exponential growth of classes when combining all possible variations of both hierarchies.

Let's consider an example of the Bridge Pattern to represent different types of shapes and different drawing methods for those shapes:

// Shape abstraction
abstract class Shape {
    protected DrawingAPI drawingAPI;

    public Shape(DrawingAPI drawingAPI) {
        this.drawingAPI = drawingAPI;
    }

    public abstract void draw();
}

// Concrete shape: Circle
class Circle extends Shape {
    private int x, y, radius;

    public Circle(int x, int y, int radius, DrawingAPI drawingAPI) {
        super(drawingAPI);
        this.x = x;
        this.y = y;
        this.radius = radius;
    }

    @Override
    public void draw() {
        drawingAPI.drawCircle(x, y, radius);
    }
}

// Concrete shape: Square
class Square extends Shape {
    private int x, y, side;

    public Square(int x, int y, int side, DrawingAPI drawingAPI) {
        super(drawingAPI);
        this.x = x;
        this.y = y;
        this.side = side;
    }

    @Override
    public void draw() {
        drawingAPI.drawSquare(x, y, side);
    }
}

// Drawing API interface
interface DrawingAPI {
    void drawCircle(int x, int y, int radius);
    void drawSquare(int x, int y, int side);
}

// Concrete Drawing API: OpenGL implementation
class OpenGLDrawingAPI implements DrawingAPI {
    @Override
    public void drawCircle(int x, int y, int radius) {
        System.out.println("Drawing Circle in OpenGL at (" + x + ", " + y + ") with radius " + radius);
    }

    @Override
    public void drawSquare(int x, int y, int side) {
        System.out.println("Drawing Square in OpenGL at (" + x + ", " + y + ") with side " + side);
    }
}

// Concrete Drawing API: SVG implementation
class SVGDrawingAPI implements DrawingAPI {
    @Override
    public void drawCircle(int x, int y, int radius) {
        System.out.println("Drawing Circle in SVG at (" + x + ", " + y + ") with radius " + radius);
    }

    @Override
    public void drawSquare(int x, int y, int side) {
        System.out.println("Drawing Square in SVG at (" + x + ", " + y + ") with side " + side);
    }
}

// Client Code
public class Client {
    public static void main(String[] args) {
        // Creating shapes using different drawing APIs
        DrawingAPI openGL = new OpenGLDrawingAPI();
        DrawingAPI svg = new SVGDrawingAPI();

        Shape circle1 = new Circle(10, 20, 5, openGL);
        Shape circle2 = new Circle(50, 30, 8, svg);

        Shape square1 = new Square(15, 25, 10, openGL);
        Shape square2 = new Square(40, 35, 12, svg);

        // Drawing shapes
        circle1.draw(); // Output: Drawing Circle in OpenGL at (10, 20) with radius 5
        circle2.draw(); // Output: Drawing Circle in SVG at (50, 30) with radius 8

        square1.draw(); // Output: Drawing Square in OpenGL at (15, 25) with side 10
        square2.draw(); // Output: Drawing Square in SVG at (40, 35) with side 12
    }
}

Decorator Pattern

The Decorator Pattern is a structural design pattern that allows behavior to be added to individual objects without modifying their class. It allows you to extend the functionality of objects dynamically at runtime by wrapping them with decorator objects.

// Component interface
interface Coffee {
    double getCost();
    String getDescription();
}

// Concrete component: Espresso
class Espresso implements Coffee {
    @Override
    public double getCost() {
        return 2.0;
    }

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

// Concrete component: Decaf
class Decaf implements Coffee {
    @Override
    public double getCost() {
        return 2.5;
    }

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

// Decorator class: CoffeeDecorator
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee decoratedCoffee) {
        this.decoratedCoffee = decoratedCoffee;
    }

    @Override
    public double getCost() {
        return decoratedCoffee.getCost();
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription();
    }
}

// Concrete decorator: Milk
class Milk extends CoffeeDecorator {
    public Milk(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

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

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

// Concrete decorator: Chocolate
class Chocolate extends CoffeeDecorator {
    public Chocolate(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    @Override
    public double getCost() {
        return super.getCost() + 1.0;
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", Chocolate";
    }
}

// Client Code
public class CoffeeShop {
    public static void main(String[] args) {
        // Ordering an Espresso with Milk and Chocolate
        Coffee espresso = new Espresso();
        Coffee espressoWithMilkAndChocolate = new Chocolate(new Milk(espresso));

        System.out.println("Ordered: " + espressoWithMilkAndChocolate.getDescription());
        System.out.println("Cost: $" + espressoWithMilkAndChocolate.getCost());
    }
}

We also have an abstract CoffeeDecorator class that extends the Coffee interface and acts as the Decorator. Concrete decorators, such as Milk and Chocolate, extend the CoffeeDecorator class. Each decorator adds specific behavior to the coffee by overriding the getCost and getDescription methods.

In the Client code, we order an Espresso and then wrap it with decorators Milk and Chocolate. The decorators add the cost and description of milk and chocolate to the original Espresso. The final output shows the ordered coffee with toppings and the total cost.

The Decorator Pattern allows us to add or remove responsibilities from objects at runtime without modifying their classes. It promotes a flexible and scalable design by composing objects with different combinations of behaviors dynamically. This pattern is particularly useful when you want to extend the behavior of individual objects without creating a large number of subclasses or modifying existing classes.

The Chain of Responsibility Pattern

It is a behavioral design pattern that creates a chain of receiver objects to handle a request. The request is passed through the chain until it is handled by one of the receivers. Each receiver in the chain has the option to handle the request or pass it to the next receiver in the chain.

Eg: Chain of responsibility principle implemented using Singleton Classes for implementing Rule Engine for tic-tac-toe GKCS. To implement rules of optimal strategy in a specific order.

Offensive placement first, then defensive, then forking, followed by center and corners/

Observer Pattern

The Observer Pattern is a behavioral design pattern that establishes a one-to-many dependency between objects. When the state of one object (subject) changes, all its dependents (observers) are notified and updated automatically. It allows multiple objects to be notified when a subject's state changes, promoting loose coupling between objects.

Let's consider an example of a weather station that notifies multiple weather display panels when the weather changes:

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

// Subject (Observable) interface
interface WeatherStation {
    void addObserver(WeatherObserver observer);
    void removeObserver(WeatherObserver observer);
    void notifyObservers();
}

// Concrete Subject: WeatherStationImpl
class WeatherStationImpl implements WeatherStation {
    private double temperature;
    private double humidity;
    private List<WeatherObserver> observers = new ArrayList<>();

    public void setWeather(double temperature, double humidity) {
        this.temperature = temperature;
        this.humidity = humidity;
        notifyObservers();
    }

    @Override
    public void addObserver(WeatherObserver observer) {
        observers.add(observer);
    }

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

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

// Observer interface
interface WeatherObserver {
    void update(double temperature, double humidity);
}

// Concrete Observer: WeatherDisplayPanel
class WeatherDisplayPanel implements WeatherObserver {
    private double temperature;
    private double humidity;

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

    private void display() {
        System.out.println("Weather Display Panel:");
        System.out.println("Temperature: " + temperature + "°C");
        System.out.println("Humidity: " + humidity + "%");
    }
}

// Client Code
public class WeatherApp {
    public static void main(String[] args) {
        // Creating the weather station
        WeatherStation weatherStation = new WeatherStationImpl();

        // Creating weather display panels
        WeatherObserver panel1 = new WeatherDisplayPanel();
        WeatherObserver panel2 = new WeatherDisplayPanel();

        // Adding display panels as observers to the weather station
        weatherStation.addObserver(panel1);
        weatherStation.addObserver(panel2);

        // Changing the weather
        weatherStation.setWeather(25.5, 65.0);
    }
}

Eg: pubsub type; similar to GKCS eventbus

Strategy Pattern

The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows clients to choose the appropriate algorithm at runtime without modifying the client's code.

// Strategy interface
interface PaymentStrategy {
    void pay(double amount);
}

// Concrete strategies: CreditCardPayment and PayPalPayment
class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;
    private String expiryDate;

    public CreditCardPayment(String cardNumber, String expiryDate) {
        this.cardNumber = cardNumber;
        this.expiryDate = expiryDate;
    }

    @Override
    public void pay(double amount) {
        System.out.println("Paying " + amount + " with credit card " + cardNumber);
    }
}

class PayPalPayment implements PaymentStrategy {
    private String email;

    public PayPalPayment(String email) {
        this.email = email;
    }

    @Override
    public void pay(double amount) {
        System.out.println("Paying " + amount + " with PayPal account " + email);
    }
}

// Context class: ShoppingCart
class ShoppingCart {
    private PaymentStrategy paymentStrategy;

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

    public void checkout(double totalAmount) {
        paymentStrategy.pay(totalAmount);
    }
}

// Client Code
public class Client {
    public static void main(String[] args) {
        // Creating payment strategies
        PaymentStrategy creditCardPayment = new CreditCardPayment("1234 5678 9012 3456", "12/24");
        PaymentStrategy payPalPayment = new PayPalPayment("[email protected]");

        // Creating a shopping cart
        ShoppingCart cart = new ShoppingCart();

        // Using different payment strategies
        cart.setPaymentStrategy(creditCardPayment);
        cart.checkout(100.0); // Output: Paying 100.0 with credit card 1234 5678 9012 3456

        cart.setPaymentStrategy(payPalPayment);
        cart.checkout(50.0); // Output: Paying 50.0 with PayPal account [email protected]
    }
}

Front Controller Pattern

Front Controller Pattern is a design pattern in which request handling is centralized. All client requests go to the front controller which contains a mapping of all controllers/servlets. The front controller maps the client request to the relevant controller/servlet, receives the results back, and renders the view to the client.

In Spring DispatcherServlet is the front controller. It receives all the requests from the client and forwards them to the appropriate controller.

The Mediator Pattern

It is a behavioral design pattern that promotes loose coupling between objects by centralizing their communication through a mediator object. It allows objects to interact with each other without knowing each other's details. The mediator acts as an intermediary that encapsulates the communication logic between objects, reducing direct dependencies between them.

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

// Mediator interface
interface Chatroom {
    void sendMessage(String message, User sender);
    void addUser(User user);
}

// Concrete Mediator: ChatroomImpl
class ChatroomImpl implements Chatroom {
    private List<User> users = new ArrayList<>();

    @Override
    public void sendMessage(String message, User sender) {
        for (User user : users) {
            if (user != sender) {
                user.receiveMessage(message);
            }
        }
    }

    @Override
    public void addUser(User user) {
        users.add(user);
    }
}

// Colleague interface
interface User {
    void sendMessage(String message);
    void receiveMessage(String message);
}

// Concrete Colleague: ChatUser
class ChatUser implements User {
    private String name;
    private Chatroom chatroom;

    public ChatUser(String name, Chatroom chatroom) {
        this.name = name;
        this.chatroom = chatroom;
        chatroom.addUser(this);
    }

    @Override
    public void sendMessage(String message) {
        chatroom.sendMessage(message, this);
    }

    @Override
    public void receiveMessage(String message) {
        System.out.println(name + " received message: " + message);
    }
}

// Client Code
public class Client {
    public static void main(String[] args) {
        // Creating a chatroom mediator
        Chatroom chatroom = new ChatroomImpl();

        // Creating users and adding them to the chatroom
        User user1 = new ChatUser("John", chatroom);
        User user2 = new ChatUser("Alice", chatroom);
        User user3 = new ChatUser("Bob", chatroom);

        // Sending messages through the chatroom mediator
        user1.sendMessage("Hello everyone!"); // Output: Alice received message: Hello everyone!
                                            //         Bob received message: Hello everyone!
        user2.sendMessage("Hi John!");       // Output: John received message: Hi John!
        user3.sendMessage("Hey there!");     // Output: John received message: Hey there!
                                            //         Alice received message: Hey there!
    }
}

In this example, we have a Chatroom interface representing the Mediator in the Mediator Pattern. It defines methods for sending messages to users and adding users to the chatroom. The concrete mediator implementation is ChatroomImpl, which maintains a list of users and handles the message distribution.

We also have a User interface representing the Colleague in the Mediator Pattern. It defines methods for sending and receiving messages. The concrete colleague implementation is ChatUser, which represents a user in the chatroom. Each ChatUser registers itself with the chatroom mediator upon creation.

In the Client code, we create a ChatroomImpl as the mediator and multiple ChatUser instances as users. Each user sends a message through the chatroom mediator, and the mediator broadcasts the message to all other users except the sender.

The Mediator Pattern promotes the centralization of communication logic, allowing objects to interact with each other in a more decoupled and maintainable way. It is particularly useful in scenarios where there are multiple interactions between objects and you want to reduce direct dependencies between them.

Comparison between mediator & observer

  • Problem Solving: While both patterns deal with object communication, the Observer pattern is more about synchronizing state changes across objects, whereas the Mediator focuses on facilitating interaction between objects without them needing to reference each other directly.
  • Decoupling: Both patterns promote decoupling, but they do it differently. The Observer allows subjects and observers to remain independent, while the Mediator centralizes the interaction logic.
  • Direction of Communication: The Observer pattern typically involves one-way communication from the subject to observers. In contrast, the Mediator pattern enables two-way communication between the objects and the mediator.

Momento pattern

The Memento Pattern is a behavioral design pattern that allows an object's state to be captured and restored later without revealing its internal structure. It provides the ability to save and restore the state of an object, making it possible to undo and redo operations.

Let's consider an example of a text editor that allows users to save and restore the state of a document:

import java.util.Stack;

// Memento: Represents the state of the document
class DocumentMemento {
    private String content;

    public DocumentMemento(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }
}

// Originator: Represents the object whose state we want to save and restore
class TextEditor {
    private String content;

    public void setContent(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }

    public DocumentMemento save() {
        return new DocumentMemento(content);
    }

    public void restore(DocumentMemento memento) {
        content = memento.getContent();
    }
}

// Caretaker: Manages the mementos
class Caretaker {
    private Stack<DocumentMemento> mementos = new Stack<>();

    public void saveState(DocumentMemento memento) {
        mementos.push(memento);
    }

    public DocumentMemento restoreState() {
        if (!mementos.isEmpty()) {
            return mementos.pop();
        }
        return null;
    }
}

// Client Code
public class Client {
    public static void main(String[] args) {
        // Creating a text editor
        TextEditor textEditor = new TextEditor();
        Caretaker caretaker = new Caretaker();

        // Editing the document and saving states
        textEditor.setContent("This is the initial content.");
        caretaker.saveState(textEditor.save());

        textEditor.setContent("Editing the content.");
        caretaker.saveState(textEditor.save());

        textEditor.setContent("Making more changes.");
        caretaker.saveState(textEditor.save());

        // Restoring previous states
        textEditor.restore(caretaker.restoreState());
        System.out.println(textEditor.getContent()); // Output: Making more changes.

        textEditor.restore(caretaker.restoreState());
        System.out.println(textEditor.getContent()); // Output: Editing the content.

        textEditor.restore(caretaker.restoreState());
        System.out.println(textEditor.getContent()); // Output: This is the initial content.
    }
}

In this example, we have a DocumentMemento class representing the Memento. It encapsulates the state of the TextEditor object, which is the Originator. The TextEditor class allows users to save and restore the state by using the save and restore methods, respectively.

The Caretaker class is responsible for managing the mementos. It maintains a stack of mementos, allowing users to save states (saveState) and restore the most recent state (restoreState).

In the Client code, we create a TextEditor instance and perform some edits on the document, saving its state using the Caretaker. We then restore previous states using the mementos saved in the Caretaker. This allows us to undo changes and revert to previous versions of the document.

The Memento Pattern provides an easy way to implement undo and redo functionality in applications and is useful when you need to save and restore the state of an object without exposing its internal details. It promotes separation of concerns and improves the maintainability of the code.

Eg: undo moves in chess, tictactoe

State pattern

The State Pattern is a behavioral design pattern that allows an object to change its behavior when its internal state changes. It encapsulates each state in a separate class and delegates the behavior to the current state object. This pattern promotes loose coupling and simplifies the code by avoiding conditional statements based on the object's state.

Let's consider an example of a simple audio player that can be in different states (Playing, Paused, Stopped) and behaves differently based on its current state:

// State interface
interface AudioPlayerState {
    void play();
    void pause();
    void stop();
}

// Concrete State: PlayingState
class PlayingState implements AudioPlayerState {
    @Override
    public void play(AudioPlayer player) {
        System.out.println("Audio is already playing.");
    }
    @Override
    public void pause(AudioPlayer player) {
        System.out.println("Audio paused.");
        player.setState(new PausedState());
    }
    @Override
    public void stop(AudioPlayer player) {
        System.out.println("Audio stopped.");
        player.setState(new StoppedState());
    }
}

// Concrete State: PausedState
class PausedState implements AudioPlayerState {
    @Override
    public void play(AudioPlayer player) {
        System.out.println("Audio resumed.");
        player.setState(new PlayingState());
    }
    @Override
    public void pause(AudioPlayer player) {
        System.out.println("Audio is already paused.");
    }
    @Override
    public void stop(AudioPlayer player) {
        System.out.println("Audio stopped.");
        player.setState(new StoppedState());
    }
}

// Concrete State: StoppedState
class StoppedState implements AudioPlayerState {
    @Override
    public void play(AudioPlayer player) {
        System.out.println("Audio started playing.");
        player.setState(new PlayingState());
    }
    @Override
    public void pause(AudioPlayer player) {
        System.out.println("Audio is not playing. Cannot pause.");
    }
    @Override
    public void stop(AudioPlayer player) {
        System.out.println("Audio is already stopped.");
    }
}

// Context: AudioPlayer
class AudioPlayer {
    private AudioPlayerState currentState;

    public AudioPlayer() {
        currentState = new StoppedState();
    }

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

    public void play() {
        currentState.play();
    }

    public void pause() {
        currentState.pause();
    }

    public void stop() {
        currentState.stop();
    }
}

// Client Code
public class Client {
    public static void main(String[] args) {
        AudioPlayer audioPlayer = new AudioPlayer();

        // Initial state: Stopped
        audioPlayer.play(); // Output: Audio started playing.

        // Change state: Playing
        audioPlayer.pause(); // Output: Audio paused.
        
        // Change state: Paused
        audioPlayer.play(); // Output: Audio resumed.

        // Change state: Playing
        audioPlayer.stop(); // Output: Audio stopped.

        // Change state: Stopped
        audioPlayer.pause(); // Output: Audio is not playing. Cannot pause.
    }
}

In this example, we have a AudioPlayerState interface representing the State in the State Pattern. It defines methods for playing, pausing, and stopping the audio player for each state.

We have three concrete state classes: PlayingState, PausedState, and StoppedState, each representing the different states of the audio player.

The AudioPlayer class acts as the context and maintains the current state. It delegates the behavior to the current state object when a method is called (e.g., play, pause, stop).

In the Client code, we create an AudioPlayer and interact with it, changing its state between Playing, Paused, and Stopped. The audio player behaves differently based on its current state.

The State Pattern promotes clean separation of concerns by encapsulating the behavior of each state in separate classes. It simplifies the code and makes it more maintainable by eliminating the need for multiple conditional statements based on the object's state. Additionally, it allows easy addition of new states without modifying existing code.

Template pattern

The Template Pattern is a behavioral design pattern that defines the structure of an algorithm in a method but delegates some steps to subclasses. It allows subclasses to redefine certain steps of the algorithm without changing its overall structure. The template pattern promotes code reuse and provides a flexible way to implement variations of an algorithm.

Let's consider an example of a simple recipe template for making different types of sandwiches:

// Template class: SandwichRecipe
abstract class SandwichRecipe {
    public final void makeSandwich() {
        prepareBread();
        addIngredients();
        addCondiments();
        wrapSandwich();
    }

    protected abstract void addIngredients();

    protected abstract void addCondiments();

    protected void prepareBread() {
        System.out.println("Prepare bread slices.");
    }

    protected void wrapSandwich() {
        System.out.println("Wrap the sandwich.");
    }
}

// Concrete Template: VeggieSandwich
class VeggieSandwich extends SandwichRecipe {
    @Override
    protected void addIngredients() {
        System.out.println("Add veggies: lettuce, tomato, cucumber.");
    }

    @Override
    protected void addCondiments() {
        System.out.println("Add condiments: mayo, mustard.");
    }
}

// Concrete Template: GrilledCheeseSandwich
class GrilledCheeseSandwich extends SandwichRecipe {
    @Override
    protected void addIngredients() {
        System.out.println("Add cheese slices.");
    }

    @Override
    protected void addCondiments() {
        System.out.println("Add condiments: ketchup.");
    }

    @Override
    protected void wrapSandwich() {
        System.out.println("Wrap the grilled cheese sandwich in foil.");
    }
}

// Client Code
public class Client {
    public static void main(String[] args) {
        SandwichRecipe veggieSandwich = new VeggieSandwich();
        veggieSandwich.makeSandwich();

        System.out.println();

        SandwichRecipe grilledCheese = new GrilledCheeseSandwich();
        grilledCheese.makeSandwich();
    }
}

In this example, we have an abstract class SandwichRecipe representing the Template in the Template Pattern. It defines a template method makeSandwich() that encapsulates the steps for making a sandwich. Some steps, like prepareBread() and wrapSandwich(), have a default implementation, while others, addIngredients() and addCondiments(), are abstract and must be implemented by concrete subclasses.

We have two concrete sandwich recipes: VeggieSandwich and GrilledCheeseSandwich, both extending SandwichRecipe. Each concrete class implements the abstract methods to customize the steps for making their specific type of sandwich.

In the Client code, we create instances of VeggieSandwich and GrilledCheeseSandwich and call the makeSandwich() method. This method follows the template structure defined in the SandwichRecipe, but the specific steps are implemented based on the concrete class.

The Template Pattern promotes code reuse by defining the high-level algorithm structure in the abstract class and allowing concrete subclasses to implement specific steps. It provides a consistent approach to build different variations of an algorithm without duplicating code.

Interpreter Pattern

The Interpreter Pattern is a behavioral design pattern that is used to interpret and evaluate expressions in a language. It defines a grammar for the language and provides a way to interpret and execute sentences in the language. This pattern is commonly used to implement domain-specific languages or to parse and evaluate complex expressions.

import java.util.Map;

// Abstract Expression interface
interface Expression {
    int interpret(Map<String, Integer> variables);
}

// Terminal Expression: NumberExpression
class NumberExpression implements Expression {
    private int value;

    public NumberExpression(int value) {
        this.value = value;
    }

    @Override
    public int interpret(Map<String, Integer> variables) {
        return value;
    }
}

// Non-terminal Expression: AddExpression
class AddExpression implements Expression {
    private Expression left;
    private Expression right;

    public AddExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret(Map<String, Integer> variables) {
        return left.interpret(variables) + right.interpret(variables);
    }
}

// Non-terminal Expression: SubtractExpression
class SubtractExpression implements Expression {
    private Expression left;
    private Expression right;

    public SubtractExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret(Map<String, Integer> variables) {
        return left.interpret(variables) - right.interpret(variables);
    }
}

// Client Code
public class Client {
    public static void main(String[] args) {
        // Creating expressions: 2 + 3 and (4 - 2) + 5
        Expression expression1 = new AddExpression(new NumberExpression(2), new NumberExpression(3));
        Expression expression2 = new AddExpression(new SubtractExpression(new NumberExpression(4), new NumberExpression(2)), new NumberExpression(5));

        // Creating a context with variable values
        Map<String, Integer> variables = Map.of("x", 10, "y", 7);

        // Evaluating the expressions with the context
        int result1 = expression1.interpret(variables);
        int result2 = expression2.interpret(variables);

        System.out.println("Result 1: " + result1); // Output: Result 1: 5
        System.out.println("Result 2: " + result2); // Output: Result 2: 7
    }
}

In this example, we have an Expression interface representing the Abstract Expression in the Interpreter Pattern. It defines a common interpret method that all concrete expression classes must implement.

We have two non-terminal expression classes: AddExpression and SubtractExpression, each representing addition and subtraction operations between two expressions. These classes take two expressions as inputs and evaluate them accordingly.

We also have a terminal expression class: NumberExpression, which represents a numeric value.

In the Client code, we create instances of different expressions representing arithmetic operations. We then create a context with variable values and evaluate the expressions with the context. The expressions are interpreted and executed based on the variable values provided in the context.

The Interpreter Pattern allows you to build interpreters for complex expressions by defining a grammar and mapping it to classes representing different parts of the grammar. It enables the evaluation of expressions in a flexible and extensible manner, making it suitable for tasks such as parsing, interpreting domain-specific languages, or evaluating complex expressions in various contexts.

Design Patterns

  • Builder pattern: While writing a constructor, if I have to write multiple constructors because i could have different combinations of parameters => write a builder class.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment