Design Pattern

·

13 min read

Design Pattern

Design patterns are general reusable solutions to common problems that occur during software development. They represent best practices evolved over time by experienced developers. There are several design patterns, categorized into three main types: creational, structural, and behavioral patterns.

  1. Creational Patterns:

    • Singleton Pattern: Ensures a class has only one instance and provides a global point of access to it.

    • Factory Method Pattern: Defines an interface for creating an object but leaves the choice of its type to the subclasses, creating an instance of a class.

    • Abstract Factory Pattern: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.

  2. Structural Patterns:

    • Adapter Pattern: Allows the interface of an existing class to be used as another interface.

    • Decorator Pattern: Attaches additional responsibilities to an object dynamically.

    • Composite Pattern: Composes objects into tree structures to represent part-whole hierarchies.

  3. Behavioral Patterns:

    • Observer Pattern: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

    • Strategy Pattern: Defines a family of algorithms, encapsulates each one, and makes them interchangeable.

    • Command Pattern: Encapsulates a request as an object, thereby allowing for parameterization of clients with different requests, queuing of requests, and logging of the parameters.

These patterns help developers create more maintainable and scalable software by promoting modular, reusable, and clean code. It's essential to choose the right pattern based on the specific problem or requirement at hand.

Explain each of them with java code and explain when and how to use them

Certainly! Let's go through each design pattern with a brief explanation and provide a simple Java code example for each.

1. Singleton Pattern:

Explanation: Ensures that a class has only one instance and provides a global point of access to it.

Java Code:

public class Singleton {
    private static Singleton instance;

    private Singleton() {
        // private constructor to prevent instantiation
    }

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

When to use: Use the Singleton pattern when you want to ensure that a class has only one instance, and you want a global point of access to that instance. Commonly used for logging, driver objects, caching, thread pools, and database connections.

2. Factory Method Pattern:

Explanation: Defines an interface for creating an object but leaves the choice of its type to the subclasses, creating an instance of a class.

Java Code:

// Product interface
public interface Product {
    void create();
}

// ConcreteProductA class
public class ConcreteProductA implements Product {
    @Override
    public void create() {
        System.out.println("ConcreteProductA created.");
    }
}

// ConcreteProductB class
public class ConcreteProductB implements Product {
    @Override
    public void create() {
        System.out.println("ConcreteProductB created.");
    }
}

// Creator interface (Factory Method)
public interface Creator {
    Product factoryMethod();
}

// ConcreteCreatorA class
public class ConcreteCreatorA implements Creator {
    @Override
    public Product factoryMethod() {
        return new ConcreteProductA();
    }
}

// ConcreteCreatorB class
public class ConcreteCreatorB implements Creator {
    @Override
    public Product factoryMethod() {
        return new ConcreteProductB();
    }
}

// Client class
public class Client {
    public static void main(String[] args) {
        // Creating an instance of ConcreteCreatorA
        Creator creatorA = new ConcreteCreatorA();
        Product productA = creatorA.factoryMethod();
        productA.create();

        // Creating an instance of ConcreteCreatorB
        Creator creatorB = new ConcreteCreatorB();
        Product productB = creatorB.factoryMethod();
        productB.create();
    }
}

In this example, we have a Product interface representing the products to be created. Concrete classes ConcreteProductA and ConcreteProductB implement the Product interface. The Creator interface declares the factory method (factoryMethod) for creating a Product.

Concrete classes ConcreteCreatorA and ConcreteCreatorB implement the Creator interface and provide their own implementations of the factory method, creating instances of ConcreteProductA and ConcreteProductB, respectively.

The Client class demonstrates how to use the Factory Method Pattern by creating instances of ConcreteCreatorA and ConcreteCreatorB and then using their factory methods to create specific products (ConcreteProductA and ConcreteProductB).

When to use: Use the Factory Method pattern when a class cannot anticipate the class of objects it must create, or you want to delegate the responsibility of object instantiation to its subclasses.

3. Abstract Factory Pattern:

Explanation: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.

Java Code:

// Abstract Product A
interface ProductA {
    void create();
}

// Concrete Product A1
class ConcreteProductA1 implements ProductA {
    @Override
    public void create() {
        System.out.println("Product A1 created.");
    }
}

// Concrete Product A2
class ConcreteProductA2 implements ProductA {
    @Override
    public void create() {
        System.out.println("Product A2 created.");
    }
}

// Abstract Product B
interface ProductB {
    void create();
}

// Concrete Product B1
class ConcreteProductB1 implements ProductB {
    @Override
    public void create() {
        System.out.println("Product B1 created.");
    }
}

// Concrete Product B2
class ConcreteProductB2 implements ProductB {
    @Override
    public void create() {
        System.out.println("Product B2 created.");
    }
}

// Abstract Factory
interface AbstractFactory {
    ProductA createProductA();
    ProductB createProductB();
}

// Concrete Factory 1
class ConcreteFactory1 implements AbstractFactory {
    @Override
    public ProductA createProductA() {
        return new ConcreteProductA1();
    }

    @Override
    public ProductB createProductB() {
        return new ConcreteProductB1();
    }
}

// Concrete Factory 2
class ConcreteFactory2 implements AbstractFactory {
    @Override
    public ProductA createProductA() {
        return new ConcreteProductA2();
    }

    @Override
    public ProductB createProductB() {
        return new ConcreteProductB2();
    }
}

// Client
public class Client {
    public static void main(String[] args) {
        // Using Concrete Factory 1
        AbstractFactory factory1 = new ConcreteFactory1();
        ProductA productA1 = factory1.createProductA();
        ProductB productB1 = factory1.createProductB();
        productA1.create();
        productB1.create();

        // Using Concrete Factory 2
        AbstractFactory factory2 = new ConcreteFactory2();
        ProductA productA2 = factory2.createProductA();
        ProductB productB2 = factory2.createProductB();
        productA2.create();
        productB2.create();
    }
}

In this example:

  • ProductA and ProductB are abstract product interfaces.

  • ConcreteProductA1, ConcreteProductA2, ConcreteProductB1, and ConcreteProductB2 are concrete product implementations.

  • AbstractFactory is the abstract factory interface declaring the methods to create ProductA and ProductB.

  • ConcreteFactory1 and ConcreteFactory2 are concrete factory implementations that create families of related products.

  • The Client class demonstrates how to use the abstract factory and create instances of products from different families.

When to use: Use the Abstract Factory pattern when you want to provide an interface for creating families of related or dependent objects without specifying their concrete classes. It's especially useful when the system needs to be independent of how its objects are created, composed, and represented.

4. Adapter Pattern:

Explanation: Allows the interface of an existing class to be used as another interface.

Java Code:

// Target interface
interface Target {
    void request();
}

// Adaptee (existing class)
class Adaptee {
    void specificRequest() {
        System.out.println("Specific request from Adaptee.");
    }
}

// Adapter
class Adapter implements Target {
    private Adaptee adaptee;

    Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public void request() {
        adaptee.specificRequest();
    }
}

// Client
public class Client {
    public static void main(String[] args) {
        // Using the Target interface directly
        Target target1 = new Adapter();
        target1.request();

        // Using the Adapter to make Adaptee compatible with Target
        Adaptee adaptee = new Adaptee();
        Target target2 = new Adapter(adaptee);
        target2.request();
    }
}

In this example:

  • Target is the target interface that the client code expects.

  • Adaptee is an existing class with a method specificRequest that needs to be adapted.

  • Adapter is the adapter class that implements the Target interface and wraps an instance of Adaptee.

  • Client demonstrates how both the Target and the adapted Adaptee can be used interchangeably through the Adapter.

When the client code calls the request method on the Target, it can be fulfilled either by the ConcreteTarget (a class implementing the Target interface directly) or by the Adapter, which internally delegates the request to the specificRequest method of Adaptee.

When to use: Use the Adapter pattern when you want to make an existing class with a compatible interface work with a class that expects a different interface. It is often used to make existing classes work with others without modifying their source code.

5. Decorator Pattern:

Explanation: Attaches additional responsibilities to an object dynamically.

Java Code:

// Component interface
interface Component {
    void operation();
}

// Concrete Component
class ConcreteComponent implements Component {
    @Override
    public void operation() {
        System.out.println("ConcreteComponent operation.");
    }
}

// Decorator
abstract class Decorator implements Component {
    protected Component component;

    Decorator(Component component) {
        this.component = component;
    }

    @Override
    public void operation() {
        component.operation();
    }
}

// Concrete Decorator A
class ConcreteDecoratorA extends Decorator {
    ConcreteDecoratorA(Component component) {
        super(component);
    }

    @Override
    public void operation() {
        super.operation();
        System.out.println("ConcreteDecoratorA operation.");
    }
}

// Concrete Decorator B
class ConcreteDecoratorB extends Decorator {
    ConcreteDecoratorB(Component component) {
        super(component);
    }

    @Override
    public void operation() {
        super.operation();
        System.out.println("ConcreteDecoratorB operation.");
    }
}

// Client
public class Client {
    public static void main(String[] args) {
        // Using the Concrete Component directly
        Component component = new ConcreteComponent();
        component.operation();

        System.out.println("-----------------------");

        // Using the Decorators to add additional behavior
        Component decoratedComponentA = new ConcreteDecoratorA(new ConcreteComponent());
        decoratedComponentA.operation();

        System.out.println("-----------------------");

        Component decoratedComponentB = new ConcreteDecoratorB(new ConcreteComponent());
        decoratedComponentB.operation();

        System.out.println("-----------------------");

        // Using multiple decorators
        Component fullyDecoratedComponent = new ConcreteDecoratorA(new ConcreteDecoratorB(new ConcreteComponent()));
        fullyDecoratedComponent.operation();
    }
}

In this example:

  • Component is the interface that defines the basic behavior.

  • ConcreteComponent is the concrete implementation of Component.

  • Decorator is the abstract decorator class that implements Component and has a reference to a Component instance.

  • ConcreteDecoratorA and ConcreteDecoratorB are concrete decorator classes that add specific behavior to the decorated component.

  • Client demonstrates how to use the concrete component and decorators to create and compose objects with different combinations of behavior.

When to use: Use the Decorator pattern when you want to add new functionality to an object dynamically without altering its structure. It is an alternative to subclassing for extending behavior.

6. Composite Pattern:

Explanation: Composes objects into tree structures to represent part-whole hierarchies.

Java Code:

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

// Component interface
interface Component {
    void operation();
}

// Leaf
class Leaf implements Component {
    private String name;

    Leaf(String name) {
        this.name = name;
    }

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

// Composite
class Composite implements Component {
    private List<Component> children = new ArrayList<>();

    void add(Component component) {
        children.add(component);
    }

    void remove(Component component) {
        children.remove(component);
    }

    @Override
    public void operation() {
        for (Component component : children) {
            component.operation();
        }
    }
}

// Client
public class Client {
    public static void main(String[] args) {
        // Creating leaf components
        Component leaf1 = new Leaf("Leaf 1");
        Component leaf2 = new Leaf("Leaf 2");
        Component leaf3 = new Leaf("Leaf 3");

        // Creating composite components
        Composite composite1 = new Composite();
        Composite composite2 = new Composite();

        // Adding leaf and composite components to the first composite
        composite1.add(leaf1);
        composite1.add(leaf2);
        composite1.add(composite2);

        // Adding another leaf to the second composite
        composite2.add(leaf3);

        // Operation on the first composite triggers operations on its children
        composite1.operation();
    }
}

In this example:

  • Component is the interface that declares the common operation for both leaf and composite objects.

  • Leaf is a leaf component that implements the Component interface.

  • Composite is a composite component that can have child components, both leaf and other composites.

  • Client demonstrates how to create and use a composite structure by adding leaf and composite components to the hierarchy and performing operations on the entire structure.

When to use: Use the Composite pattern when you want to compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly.

7. Observer Pattern:

Explanation: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Java Code:

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

// Subject interface
interface Subject {
    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
    int getState();
    void setState(int state);
}

// Concrete Subject
class ConcreteSubject implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private int state;

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

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

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

    @Override
    public int getState() {
        return state;
    }

    @Override
    public void setState(int state) {
        this.state = state;
        notifyObservers();
    }
}

// Observer interface
interface Observer {
    void update();
}

// Concrete Observer
class ConcreteObserver implements Observer {
    private Subject subject;

    ConcreteObserver(Subject subject) {
        this.subject = subject;
        subject.addObserver(this);
    }

    @Override
    public void update() {
        System.out.println("Observer updated with state: " + subject.getState());
    }
}

// Client
public class Client {
    public static void main(String[] args) {
        // Creating a concrete subject
        ConcreteSubject subject = new ConcreteSubject();

        // Creating concrete observers and attaching them to the subject
        Observer observer1 = new ConcreteObserver(subject);
        Observer observer2 = new ConcreteObserver(subject);

        // Setting the state of the subject will notify all attached observers
        subject.setState(5);
    }
}

In this example:

  • Subject is the interface that declares methods for managing observers and notifying them of changes in state.

  • ConcreteSubject is the concrete subject that maintains a list of observers and notifies them when its state changes.

  • Observer is the interface that declares the update method, which is called by the subject to notify observers of changes.

  • ConcreteObserver is the concrete observer that registers itself with a subject and updates its state when notified.

  • Client demonstrates how to create a concrete subject, attach observers to it, and trigger notifications by changing the subject's state.

When to use: Use the Observer pattern when a one-to-many dependency exists between objects, and when one object changes state, all its dependents are notified and updated automatically. It is commonly used in implementing distributed event handling systems.

8. Strategy Pattern:

Explanation: Defines a family of algorithms, encapsulates each one, and makes them interchangeable.

Java Code:

// Strategy interface
interface Strategy {
    void execute();
}

// Concrete Strategy A
class ConcreteStrategyA implements Strategy {
    @Override
    public void execute() {
        System.out.println("Executing strategy A.");
    }
}

// Concrete Strategy B
class ConcreteStrategyB implements Strategy {
    @Override
    public void execute() {
        System.out.println("Executing strategy B.");
    }
}

// Context
class Context {
    private Strategy strategy;

    void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    void executeStrategy() {
        strategy.execute();
    }
}

// Client
public class Client {
    public static void main(String[] args) {
        // Creating strategies
        Strategy strategyA = new ConcreteStrategyA();
        Strategy strategyB = new ConcreteStrategyB();

        // Creating context and setting the strategy
        Context context = new Context();

        // Using strategy A
        context.setStrategy(strategyA);
        context.executeStrategy();

        // Using strategy B
        context.setStrategy(strategyB);
        context.executeStrategy();
    }
}

In this example:

  • Strategy is the interface that declares the algorithm to be executed.

  • ConcreteStrategyA and ConcreteStrategyB are concrete implementations of the Strategy interface, each representing a different algorithm.

  • Context is the class that maintains a reference to the current strategy and delegates the execution to the strategy.

When to use: Use the Strategy pattern when you want to define a family of algorithms, encapsulate each algorithm, and make them interchangeable. It allows the client to choose the appropriate algorithm at runtime.

9. Command Pattern:

Explanation: Encapsulates a request as an object, thereby allowing for parameterization of clients with different requests, queuing of requests, and logging of the parameters.

Java Code:

// Command interface
interface Command {
    void execute();
}

// Concrete Command
class ConcreteCommand implements Command {
    private Receiver receiver;

    ConcreteCommand(Receiver receiver) {
        this.receiver = receiver;
    }

    @Override
    public void execute() {
        receiver.action();
    }
}

// Receiver
class Receiver {
    void action() {
        System.out.println("Receiver is performing an action.");
    }
}

// Invoker
class Invoker {
    private Command command;

    void setCommand(Command command) {
        this.command = command;
    }

    void executeCommand() {
        command.execute();
    }
}

// Client
public class Client {
    public static void main(String[] args) {
        // Creating receiver
        Receiver receiver = new Receiver();

        // Creating command and associating it with the receiver
        Command command = new ConcreteCommand(receiver);

        // Creating invoker and setting the command
        Invoker invoker = new Invoker();
        invoker.setCommand(command);

        // Invoking the command
        invoker.executeCommand();
    }
}

In this example:

  • Command is the interface that declares the execute method.

  • ConcreteCommand is the concrete implementation of the Command interface, encapsulating a specific action to be performed by the Receiver.

  • Receiver is the class that performs the actual action.

  • Invoker is the class that invokes the command, but it doesn't need to know the details of how the command is executed.

The Client code demonstrates how to create a receiver, associate it with a command, and use an invoker to execute the command. The beauty of the Command Pattern is that it decouples the sender (Invoker) from the receiver and allows easy extension by introducing new command classes.

When to use: Use the Command pattern when you want to parameterize objects with operations, queue requests, and support undoable operations. It separates the sender and receiver of a command, allowing for decoupling and flexibility.