Design Patterns

Author

Dr. Ashish Sai

Design Patterns

Understanding Design Patterns

  • Definition: Design patterns are typical solutions to recurring design problems in software engineering.
  • Purpose: They provide a template or blueprint for solving issues that occur frequently within a given context in software design.
  • Customization: While patterns provide a general framework, they require adaptation to fit specific cases, allowing a tailored solution for unique problems.
  • Not Code Snippets: Unlike direct code solutions, patterns offer a higher-level description and methodology for solving complex design issues.

Differentiating Patterns from Algorithms

  • Algorithms: Defined as a step-by-step procedure or formula for solving a problem, typically in a finite number of steps. Comparable to a cooking recipe with a clear sequence.
  • Design Patterns: More abstract and flexible. They provide a scheme for structuring software to solve a complex problem, more like architectural blueprints.
  • Implementation Variability: The same pattern can result in different code across different applications, reflecting the specific needs and constraints of the software.

Anatomy of a Design Pattern

  • Intent: Describes the goal of the pattern and the issue it solves.
  • Motivation: Elaborates on the problem, providing context and reasons why the pattern is a suitable solution.
  • Structure of Classes: Visual or conceptual representation of the pattern’s components and their interrelationships.
  • Code Example: Provides a concrete example, typically in a widely-used programming language, demonstrating how the pattern might be implemented.
  • Applicability & Implementation Steps: Some descriptions might include when to use the pattern and steps to implement it.

Classifying Design Patterns

  • By Complexity and Detail:
    • Idioms: Specific to a programming language, addressing low-level issues and specifics.
    • Architectural Patterns: High-level patterns that guide the overall structure and organization of software systems.
  • By Purpose:
    • Creational Patterns: Concerned with object creation mechanisms.
    • Structural Patterns: Deal with object composition and the formation of larger structures.
    • Behavioral Patterns: Focus on communication between objects and responsibilities.

The Purpose Behind Design Patterns

  • Creational Patterns: Simplify object creation, making a system independent of how its objects are created, composed, and represented. Examples include Singleton and Factory Method.
  • Structural Patterns: Help to form larger structures while keeping the system flexible and efficient. They ensure that changing one part of the system does not affect other parts. Examples include Adapter and Composite.
  • Behavioral Patterns: Enhance communication between objects and help in the assignment of responsibilities between objects, making the interaction more flexible and efficient. Examples include Observer and Strategy.

The Origins and Evolution of Design Patterns

  • Historical Context: Initially described by Christopher Alexander in the context of town and building planning.
  • Adaptation to Software: The concept was adapted to software design by the “Gang of Four” (GoF) - Erich Gamma, John Vlissides, Ralph Johnson, and Richard Helm - in their seminal work “Design Patterns: Elements of Reusable Object-Oriented Software.”
  • Evolution: Since the publication of the GoF book, the idea of design patterns has evolved and expanded into various domains of software engineering.

The Discovery and Documentation of Patterns

  • Pattern Recognition: Patterns are identified when a recurring solution emerges across different projects and contexts.
  • Naming and Documentation: Once recognized, these solutions are named, documented, and shared, allowing others to apply the pattern to similar problems.
  • Community Involvement: The identification and documentation of patterns are often a community-driven process, with practitioners contributing from their experiences.

The Significance of Learning Design Patterns

  • Problem-Solving Toolkit: Provides a set of proven solutions to common problems, enhancing problem-solving skills.
  • Design Vocabulary: Establishes a common language among developers, facilitating more efficient communication and collaboration.
  • Best Practices: Encourages the adoption of best practices and principles in software design, leading to more maintainable and scalable code.

Communication and Efficiency through Patterns

  • Shared Understanding: Using pattern names and concepts helps in quickly conveying complex design structures and solutions.
  • Efficient Problem-Solving: Recognizing a problem that corresponds to a pattern allows for a quick, efficient approach to a solution.
  • Beyond Jargon: Understanding the underlying principles of each pattern is crucial, as misuse or overuse can lead to complications.

Practical Application and Recognition of Patterns

  • Recognizing Patterns in Use: Often, developers use patterns unknowingly. Learning about them helps in recognizing and properly implementing them.
  • Conscious Application: With knowledge of patterns, developers can consciously apply them to appropriate problems, enhancing the design’s quality and maintainability.
  • Continuous Learning: The field of design patterns is ever-evolving, encouraging continuous learning and adaptation of new and improved solutions.

What’s a Design Pattern?

  • Design patterns are typical solutions to common problems in software design.

  • Essence of Design Patterns

    • Blueprints: Think of them as pre-made blueprints for solving recurring design issues.
    • Customizable: They’re adaptable to solve specific problems in your code.
  • Patterns: Not Just Code

    • Conceptual: Patterns are more about the concept than the actual code.
    • Problem-Solving Tools: They provide time-tested solutions to frequent challenges in software design.

Key Characteristics of Patterns

  • Not Specific Code: A pattern is a concept, not a specific piece of code.
  • Solution to Problems: Patterns provide a standard solution to a known problem.
  • Difference from Algorithms: Unlike algorithms, patterns offer a high-level description of a solution.
  • Custom Implementation: You implement a pattern in a way that suits your program’s realities.

Components of a Pattern

Most patterns are described formally so they can be reproduced in many contexts. Common sections in a pattern description include:

  • Intent: Briefly describes the problem and the solution.
  • Motivation: Further explains the problem and the solution the pattern offers.
  • Structure: Shows the parts of the pattern and how they relate.
  • Code Example: Typically provided in popular programming languages to help understand the concept.

The Inception of Design Patterns

Patterns emerge when a solution is repeated across various projects, eventually gaining a name and detailed description.

graph TD;
    A[Design Patterns] -->|Discovered Through Repetition| B[Named & Described]
    B --> C[Applied in Various Projects]
  • Origin: The concept of patterns was first described by Christopher Alexander in “A Pattern Language: Towns, Buildings, Construction.”
  • Application: Patterns describe solutions in designing the urban environment and are later applied to software design.

Evolution and Popularization

The journey from architectural design principles to essential software design tools.

graph LR;
    A[Christopher Alexander] -->|A Pattern Language| B[Concept of Patterns]
    B -->|Adopted by 'Gang of Four'| C[Design Patterns: Elements of Reusable Object-Oriented Software]
    C -->|23 Patterns Presented| D[Popularization in Software Design]
    D --> E[Expansion & Adaptation in Various Fields]
  • Gang of Four: Erich Gamma, John Vlissides, Ralph Johnson, and Richard Helm applied design patterns to programming in 1994.
  • Legacy: Their work, commonly known as the “GoF book,” became a best-seller and foundation for numerous other object-oriented patterns.

Why Learn Design Patterns?

  • Solve Common Problems: Design patterns provide solutions to frequently encountered problems in software design.

  • Effective Communication: They offer a standard terminology for developers, enhancing team understanding and collaboration.

  • Principles of Design: Patterns help in applying principles of object-oriented design effectively.

  • Improve Code Quality: Learning patterns can lead to more efficient, readable, and maintainable code.

Problems Common Problems in Software Design Solutions Toolkit of Tested Solutions Problems->Solutions Addressed by Language Common Language for Team Communication Solutions->Language Facilitates Principles Principles of Object-Oriented Design Language->Principles Based on

Criticism of Design Patterns

Understanding the limitations and appropriate use of design patterns is crucial.

  • Language Limitations: Patterns may arise from a language’s shortcomings, like Strategy patterns being replaced by lambdas in modern languages.
  • Efficiency Concerns: Misapplying patterns can lead to complex, inefficient solutions. Context matters.
  • Overuse by Novices: Inexperienced use can lead to overcomplicated code when simpler options exist.

If all you have is a hammer, everything looks like a nail.

Design patterns are tools to be used with discretion and understanding, not as universal fixes.

Here’s a single slide in Quarto markdown format based on the “Classification of Patterns” content from the provided link. I’ve included a GraphViz diagram to visually represent the classification of design patterns:

Types of Design Patterns

Design patterns offer solutions ranging from specific, low-level idioms to high-level architectural blueprints.

  • Idioms: Language-specific, low-level patterns providing detailed solutions to coding issues.
  • Architectural Patterns: High-level strategies that shape the entire application, guiding its overall structure and behavior.

Main Categories

graph LR;
    A[Design Patterns] -->|Intent| B[Creational]
    A -->|Intent| C[Structural]
    A -->|Intent| D[Behavioral]
    B -->|Flexibility & Reuse| E[Object Creation Mechanisms]
    C -->|Efficiency & Flexibility| F[Assembling Objects into Structures]
    D -->|Communication & Responsibility| G[Object Interactions]
  • Creational Patterns: Focus on object creation mechanisms, increasing flexibility and reuse of existing code.
  • Structural Patterns: Deal with assembling objects and classes into larger structures while maintaining efficiency and flexibility.
  • Behavioral Patterns: Concerned with effective communication and the assignment of responsibilities among objects.

Creational Design Patterns

Creational patterns are fundamental for object creation in software design. They provide mechanisms that increase flexibility and reuse of existing code.

  • The Five Main Creational Patterns
    • Factory Method: Delegates the creation of objects to subclasses, promoting flexibility and integration.
    • Abstract Factory: Creates families of related or dependent objects without specifying their concrete classes.
    • Builder: Constructs complex objects step by step, allowing the creation process to create different types and representations of an object.
    • Prototype: Creates new objects by copying an existing object, known as the prototype.
    • Singleton: Ensures a class has only one instance while providing a global point of access to it.

Introduction to Factory Method

Factory Method is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. It’s also known as Virtual Constructor.

Factory Method: Intent

The Factory Method pattern is designed to:

  • Provide an interface for object creation in a superclass.

  • Allow subclasses to change the type of objects created.

  • Promote loose coupling and scalability in the codebase.

Factory Method: Problem Scenario

Imagine a logistics management application:

  • Initially handles transportation by trucks (Truck class).

  • Needs to incorporate sea transportation (Ship class) due to popular demand.

  • Directly adding new classes leads to code tightly coupled with the Truck class, making the system inflexible and hard to maintain.

public class LogisticsApp {
    // Existing code heavily reliant on the Truck class.
}

Factory Method: Solution Overview

The Factory Method pattern suggests:

  • Replacing direct object construction calls (using new) with calls to a special factory method.

  • This method is typically overridden in subclasses to create specific types of objects.

public abstract class Logistics {
    public abstract Transport createTransport();
    // Other logistics-related methods.
}

Factory Method: Implementation - Class Structure

classDiagram
    class Creator {
        +FactoryMethod() Product
    }
    class ConcreteCreator {
        +FactoryMethod() Product
    }
    class Product {
        <<interface>> 
        +Operation()
    }
    class ConcreteProduct {
        +Operation()
    }

    Creator <|-- ConcreteCreator
    Product <|-- ConcreteProduct
    Creator : -factoryMethod()
    ConcreteCreator : -factoryMethod()

This diagram represents the basic structure of the Factory Method pattern.

Factory Method: Concrete Implementation

classDiagram
    class Logistics {
        +createTransport() Transport
    }
    class RoadLogistics {
        +createTransport() Transport
    }
    class SeaLogistics {
        +createTransport() Transport
    }
    class Transport {
        <<interface>> 
        +deliver()
    }
    class Truck {
        +deliver()
    }
    class Ship {
        +deliver()
    }

    Logistics <|-- RoadLogistics
    Logistics <|-- SeaLogistics
    Transport <|-- Truck
    Transport <|-- Ship

Factory Method: Code Example - Base Classes

public interface Transport {
    void deliver();
}

public abstract class Logistics {
    public abstract Transport createTransport();
    public void planDelivery() {
        Transport transport = createTransport();
        transport.deliver();
    }
}

Factory Method: Code Example - Concrete Classes

public class Truck implements Transport {
    public void deliver() {
        System.out.println("Delivery by land in a box.");
    }
}

public class Ship implements Transport {
    public void deliver() {
        System.out.println("Delivery by sea in a container.");
    }
}

public class RoadLogistics extends Logistics {
    public Transport createTransport() {
        return new Truck();
    }
}

public class SeaLogistics extends Logistics {
    public Transport createTransport() {
        return new Ship();
    }
}

Factory Method: Applicability

Use the Factory Method when:

  • The exact types and dependencies of objects are not known beforehand.

  • You want to provide users with a way to extend internal components of a library or framework.

  • You want to save system resources by reusing existing objects instead of rebuilding them.

Factory Method: Pros and Cons

Pros:

  • Avoids tight coupling between creator and concrete products.

  • Adheres to the Single Responsibility and Open/Closed Principles.

  • Simplifies code maintenance and extension.

Cons:

  • Can lead to an increase in the number of classes due to the need for numerous subclasses.

Abstract Factory Pattern Overview

The Abstract Factory is a creational design pattern that lets you produce families of related objects without specifying their concrete classes.

Abstract Factory: The Problem

Imagine creating a furniture shop simulator with families of products like Chair, Sofa, and CoffeeTable, available in Modern, Victorian, and ArtDeco styles. The challenge is ensuring that furniture pieces match in style without changing the code for new product additions.

Abstract Factory: The Solution

Abstract Factory suggests declaring interfaces for each product (e.g., Chair, Sofa) and making all variants follow these interfaces. Factories for each variant then produce these objects without exposing the concrete classes.

classDiagram
    class AbstractFactory {
      +createChair(): Chair
      +createSofa(): Sofa
      +createCoffeeTable(): CoffeeTable
    }
    class ModernFurnitureFactory {
      +createChair(): Chair
      +createSofa(): Sofa
      +createCoffeeTable(): CoffeeTable
    }
    class VictorianFurnitureFactory {
      +createChair(): Chair
      +createSofa(): Sofa
      +createCoffeeTable(): CoffeeTable
    }
    AbstractFactory <|-- ModernFurnitureFactory
    AbstractFactory <|-- VictorianFurnitureFactory

Abstract Factory: Structure of Abstract Factory

Abstract Factory involves Abstract Products, Concrete Products, the Abstract Factory itself, and Concrete Factories corresponding to each product variant.

Abstract Factory: Abstract Products

Abstract Products declare interfaces for a set of distinct but related products forming a product family.

classDiagram
    class Chair {
      <<interface>>
      +sitOn()
    }
    class Sofa {
      <<interface>>
      +layOn()
    }
    class CoffeeTable {
      <<interface>>
      +putOn()
    }

Abstract Factory: Concrete Products

Concrete Products are implementations of abstract products, grouped by variants.

// Java Example
interface Chair {
    void sitOn();
}

class ModernChair implements Chair {
    public void sitOn() {
        // Modern chair-specific code
    }
}

class VictorianChair implements Chair {
    public void sitOn() {
        // Victorian chair-specific code
    }
}

Abstract Factory: Abstract Factory Interface

The Abstract Factory interface declares a set of creation methods for all abstract products.

// Java Example
interface FurnitureFactory {
    Chair createChair();
    Sofa createSofa();
    CoffeeTable createCoffeeTable();
}

Abstract Factory: Concrete Factories

Each Concrete Factory corresponds to a specific variant of products and creates only those product variants.

// Java Example
class ModernFurnitureFactory implements FurnitureFactory {
    public Chair createChair() {
        return new ModernChair();
    }
    public Sofa createSofa() {
        return new ModernSofa();
    }
    public CoffeeTable createCoffeeTable() {
        return new ModernCoffeeTable();
    }
}

Abstract Factory: Client Code Interaction

Clients interact with factories and products through abstract types, allowing the flexibility to change factory and product types dynamically.

// Java Example
class Application {
    private FurnitureFactory factory;
    private Chair chair;

    public Application(FurnitureFactory factory) {
        this.factory = factory;
    }

    void createUI() {
        this.chair = factory.createChair();
    }

    void paint() {
        chair.sitOn();
    }
}

Abstract Factory: Pros and Cons

  • Pros:

    • Ensures products are compatible with each other.

    • Decouples client code from concrete product classes.

    • Adheres to Single Responsibility and Open/Closed Principles.

  • Cons:

    • Can become complex due to many new interfaces and classes.

I apologize for the formatting issues. Here’s a revised version as a single Quarto markdown (.qmd) document with corrected slide formatting and appropriate code blocks for Mermaid and Java:

Factory Patterns Comparison

Design patterns provide standardized solutions but vary in their usage and complexity. From low-level idioms specific to a programming language to high-level patterns that structure the entire application, understanding these can guide the right implementation.

Creation Method

A creation method within a class is responsible for creating instances. This term generally refers to any method whose primary purpose is to create and return a new object.

public class Number {
    private int value;

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

    public Number next() {
        return new Number(this.value + 1);
    }
}

Here, next is a creation method, producing a new Number instance.

Static Creation Method

Static creation methods allow calling a method on the class itself to create an instance, often providing clarity and control over the instantiation process.

public class User {
    private int id;
    private String name, email, phone;

    private User(int id, String name, String email, String phone) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.phone = phone;
    }

    public static User fromId(int id) {
        // Fetch and construct user from id
        return new User(id, "Name", "Email", "Phone");
    }
}

fromId is a static creation method, simplifying user creation from an ID.

Simple Factory Pattern

The Simple Factory encapsulates object creation for a specific type, often based on given parameters.

classDiagram
    class SimpleFactory {
        +static createObject(type)
    }
    class ProductA
    class ProductB
    SimpleFactory --> ProductA: creates
    SimpleFactory --> ProductB: creates
public class SimpleFactory {
    public static Object createObject(String type) {
        if ("A".equals(type)) {
            return new ProductA();
        } else if ("B".equals(type)) {
            return new ProductB();
        }
        throw new IllegalArgumentException("Unknown type");
    }
}

This pattern centralizes and simplifies object creation.

Factory Method Pattern

The Factory Method pattern defines an interface for creating an object but lets subclasses alter the type of objects that will be created.

classDiagram
    class Creator {
        <<abstract>>
        +FactoryMethod()
    }
    class ConcreteCreatorA
    class ConcreteCreatorB
    Creator <|-- ConcreteCreatorA
    Creator <|-- ConcreteCreatorB
public abstract class Creator {
    public abstract Product factoryMethod();

    public String operation() {
        Product product = factoryMethod();
        return "Creator: The same creator's code has just worked with " + product.operation();
    }
}

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

This pattern provides flexibility and encapsulation by allowing subclasses to define how objects are created.

Abstract Factory Pattern

The Abstract Factory Pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.

classDiagram
    class AbstractFactory {
        <<abstract>>
        +CreateProductA()
        +CreateProductB()
    }
    class ConcreteFactory1
    class ConcreteFactory2
    AbstractFactory <|-- ConcreteFactory1
    AbstractFactory <|-- ConcreteFactory2
public interface AbstractFactory {
    AbstractProductA createProductA();
    AbstractProductB createProductB();
}

public class ConcreteFactory1 implements AbstractFactory {
    public AbstractProductA createProductA() {
        return new ProductA1();
    }

    public AbstractProductB createProductB() {
        return new ProductB1();
    }
}

This pattern is particularly useful when a system needs to be independent of how its objects are created, composed, and represented.

Introduction to Builder Pattern

The Builder is a creational design pattern for constructing complex objects step by step, allowing the production of different types and representations of an object using the same construction code.

Builder Pattern: The Problem

Complex objects require laborious, step-by-step initialization of many fields and nested objects, leading to a constructor with many parameters or scattered initialization across client code.

Builder Pattern: The Solution with Builder

classDiagram
  class Director {
    +construct(builder: Builder)
  }
  class Builder {
    +setPartA()
    +setPartB()
    +getResult(): Product
  }
  class ConcreteBuilder {
    +setPartA()
    +setPartB()
    +getResult(): Product
  }
  class Product {
    +partA
    +partB
  }
  Director --> Builder : uses
  Builder <|-- ConcreteBuilder : implements
  ConcreteBuilder --> Product : builds

The Builder pattern suggests extracting the object construction code into a separate Builder class, organizing construction into a series of steps to create complex objects.

Slide 4: Key Components

  1. Builder Interface: Declares product construction steps.

  2. Concrete Builders: Implement the steps differently.

  3. Products: The resulting objects, which don’t need to belong to the same class hierarchy.

  4. Director: Defines the order of construction steps.

Builder Pattern: Pseudocode Example

interface Builder {
    void setSeats(int number);
    void setEngine(Engine engine);
    void setTripComputer(boolean present);
    void setGPS(boolean present);
}

class CarBuilder implements Builder {
    private Car car = new Car();
    // Implement the builder methods...
    public Car getResult() {
        return car;
    }
}

class Director {
    void constructSportsCar(Builder builder) {
        builder.setSeats(2);
        builder.setEngine(new SportEngine());
        // Other steps...
    }
}

This Java pseudocode demonstrates the Builder interface and a concrete builder for cars.

Builder Pattern: When to Use the Builder

  • To avoid a “telescoping constructor” with many parameters.

  • When your product needs different representations with similar construction steps.

  • For constructing Composite trees or complex objects.

Builder Pattern: How to Implement

  1. Define common construction steps in the base builder interface.

  2. Create concrete builders for different product representations.

  3. Consider a director class for particular construction sequences.

  4. Client code associates a builder with the director, then initiates and retrieves construction.

Builder Pattern: Pros and Cons

  • Pros:

    • Step-by-step construction and deferred steps.

    • Reusable construction code for various products.

    • Isolation of complex construction code from business logic.

  • Cons:

    • Increased overall complexity due to multiple new classes.

Builder Pattern: Relations with Other Patterns

  • Factory Method and Abstract Factory: Start simple and evolve toward Builder for more complex scenarios.

  • Composite: Builder can be useful for constructing trees.

  • Bridge: Combine with Builder where the director plays the abstraction role and builders act as implementations.

Builder Pattern: Real-World Example in Java

// Example: Building a car with a Builder
CarBuilder builder = new CarBuilder();
Director director = new Director();
director.constructSportsCar(builder);
Car car = builder.getResult();

// The client can use the Builder directly for finer control
builder.setSeats(4);
builder.setGPS(true);
Car customCar = builder.getResult();

This Java example shows how a client might use a Builder and Director to construct a standard car and then customize another car.

Introduction to Prototype Pattern

The Prototype is a creational design pattern allowing you to copy existing objects without making the code dependent on their classes. This pattern is particularly useful when object creation is more costly than copying an existing instance.

Prototype Pattern: The Problem

Creating an exact copy of an object can be complex, especially when objects contain private fields or are part of complex object trees. Direct copying requires knowledge of the object’s class and often leads to tightly coupled code.

Prototype Pattern: Prototype Solution

classDiagram
    class Prototype {
      +clone():Prototype
    }
    class ConcretePrototype {
      +clone():Prototype
    }
    Prototype <|-- ConcretePrototype

The Prototype pattern suggests adding a clone method to the object’s class, which creates a new object and copies all field values to the new instance. This approach decouples the code from the object’s class.

Prototype Pattern: Real-World Analogy

Just like mitotic cell division in biology, where a cell divides to create a copy of itself, the Prototype pattern allows an object to produce a copy (clone) of itself, maintaining the same state as the original.

Prototype Pattern: Prototype Structure - Basic Implementation

classDiagram
    class Prototype {
      +clone():Prototype
    }
    class ConcretePrototype {
      +clone():Prototype
    }
    class Client {
      -prototype : Prototype
      +operation():void
    }
    Prototype <|-- ConcretePrototype
    Client --> Prototype : uses
  • Prototype: Declares the cloning method.

  • Concrete Prototype: Implements the cloning method.

  • Client: Produces a copy of the object through the prototype interface.

Prototype Pattern: Prototype Registry

classDiagram
    class PrototypeRegistry {
      -registry: HashMap
      +getPrototype(String): Prototype
    }
    PrototypeRegistry --> Prototype : clones

A Prototype Registry is used for easy access to frequently used prototypes. It stores a set of pre-built, ready-to-copy objects.

Prototype Pattern: Cloning in Java - Base Prototype

abstract class Shape {
    int X, Y;
    String color;

    Shape() {
        // Constructor...
    }

    Shape(Shape source) {
        this.X = source.X;
        this.Y = source.Y;
        this.color = source.color;
    }

    abstract Shape clone();
}

The base Shape class provides a framework for cloning, including a method to clone itself.

Prototype Pattern: Cloning in Java - Concrete Prototype

class Rectangle extends Shape {
    int width, height;

    Rectangle(Rectangle source) {
        super(source);
        this.width = source.width;
        this.height = source.height;
    }

    @Override
    Shape clone() {
        return new Rectangle(this);
    }
}

The Rectangle class extends Shape and implements the clone method to return a new instance of itself.

Prototype Pattern: Applicability of Prototype Pattern

Use the Prototype pattern when your code shouldn’t depend on the concrete classes of objects that need to be copied. It’s particularly useful when dealing with objects provided by third-party code or when you want to reduce the number of subclasses for object configuration.

Prototype Pattern: Implementation Steps and Considerations

  1. Declare the Prototype interface and clone method: Ensure that the clone method is present in all classes of the hierarchy.

  2. Implement cloning in subclasses: Override the clone method appropriately to copy object data.

  3. Use the Prototype Registry for frequent prototypes: Optionally, manage prototypes centrally to simplify object creation.

Creating a series of ten slides in Quarto markdown to thoroughly explain the Singleton pattern from the provided webpage, including Mermaid.js class diagrams and Java code samples. Each slide will delve into different aspects of the Singleton pattern, offering a detailed and comprehensive understanding.

Introduction to Singleton Pattern

Singleton is a creational design pattern focused on ensuring a class has only one instance while providing a global access point to this instance. It’s particularly useful for controlling access to shared resources, such as database connections.

Singleton Pattern: The Problem

The Singleton pattern addresses two primary issues: ensuring a single instance and providing a global access point. These capabilities are vital for controlling access to shared resources and preventing inconsistencies in the application state.

Singleton Pattern: Singleton Solution

classDiagram
    class Singleton {
        -static instance: Singleton
        -Singleton()
        +static getInstance(): Singleton
    }

Singleton implementation involves making the constructor private and providing a static method that returns the same instance.

Java Code:

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

Singleton Pattern: Real-World Analogy

The government serves as an analogy for the Singleton pattern. A country has one official government, acting as the central point of access and control, similar to how Singleton provides a single, globally accessible instance.

Singleton Pattern: Structure of Singleton

classDiagram
    class Singleton {
        -static instance: Singleton
        -Singleton()
        +static getInstance(): Singleton
        +businessMethod()
    }

The structure includes a private constructor and a static method for instance access, ensuring controlled creation and access.

Singleton Pattern: Implementation Steps

  1. Private Instance: Add a private static field to store the singleton instance.

  2. Public Creation Method: Implement a public static method for retrieving the singleton instance.

  3. Lazy Initialization: Initialize the singleton lazily to ensure it’s created only when needed.

Singleton Pattern: Lazy Initialization

Implementing lazy initialization ensures the Singleton instance is created only when it’s first requested, optimizing resource usage and application performance.

Java Code:

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

Singleton Pattern: Pros and Cons

Pros: - Guaranteed single instance.

  • Global access point.

  • Lazy initialization.

Cons: - Single Responsibility Principle violation.

  • Can mask bad design.

  • Requires special handling in multithreaded scenarios.

Singleton Pattern: Relations with Other Patterns

  • Facade: Often implemented as a Singleton.

  • Flyweight: Similar but with key differences like multiple instances.

  • Abstract Factories, Builders, and Prototypes: Can be Singletons to control instances.

Singleton Pattern: Usage in Java

Java’s implementation of the Singleton pattern ensures a single instance with global access, demonstrating its practical use in controlling shared resources and maintaining consistency across an application.

Java Code:

public class Database {
    private static Database instance;
    private Database() {}
    public static synchronized Database getInstance() {
        if (instance == null) {
            instance = new Database();
        }
        return instance;
    }
    public void query(String sql) {
        // Implementation here
    }
}

This example shows a Singleton used for database connections, a common use case in many applications.

Structural Patterns

Introduction to the Adapter Pattern

Adapter, also known as Wrapper, is a structural design pattern that enables objects with incompatible interfaces to collaborate.

Adapter Pattern: Intent of Adapter Pattern

The Adapter pattern allows objects with incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces, effectively allowing them to communicate.

Adapter Pattern: The Problem

Imagine a stock market monitoring app that downloads data in XML format. Introducing a 3rd-party analytics library, which only accepts JSON, presents a format incompatibility issue.

classDiagram
    class StockMarketApp {
        -XMLData()
    }
    class AnalyticsLibrary {
        +JSONData()
    }

Adapter Pattern: The Solution

Creating an adapter converts the interface of one object (XML) so that another object (Analytics Library) can understand it, enabling collaboration between the two.

classDiagram
    class XMLData
    class JSONData
    class Adapter {
        +ConvertXMLToJSON()
    }
    XMLData --|> Adapter
    Adapter --|> JSONData

Adapter Pattern: Real-World Analogy

A power plug adapter allows a device with a US plug to fit into a European socket. Similarly, the Adapter pattern lets incompatible classes work together.

Adapter Pattern: Object Adapter Structure

classDiagram
    class Client
    class ClientInterface
    class Service
    class Adapter
    Client --|> ClientInterface
    Service --|> Adapter
    Adapter --|> ClientInterface

The object adapter uses composition to connect the interfaces of Client and Service.

Adapter Pattern: Class Adapter Structure

classDiagram
    class Client
    class Service
    class Adapter {
        +ServiceMethod()
    }
    Client <|-- Adapter
    Service <|-- Adapter

The class adapter uses multiple inheritance to adapt the interface, applicable only in languages supporting multiple inheritance.

Adapter Pattern: Implementing Adapter Pattern in Java

// Existing interfaces
class RoundHole {
    private double radius;

    public RoundHole(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    public boolean fits(RoundPeg peg) {
        return this.getRadius() >= peg.getRadius();
    }
}

class RoundPeg {
    private double radius;

    public RoundPeg(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }
}

// Incompatible class
class SquarePeg {
    private double width;

    public SquarePeg(double width) {
        this.width = width;
    }

    public double getWidth() {
        return width;
    }
}

// Adapter class
class SquarePegAdapter extends RoundPeg {
    private SquarePeg peg;

    public SquarePegAdapter(SquarePeg peg) {
        this.peg = peg;
    }

    @Override
    public double getRadius() {
        return peg.getWidth() * Math.sqrt(2) / 2;
    }
}

Adapter Pattern: Applicability of Adapter Pattern

Use the Adapter pattern when you want to use an existing class, but its interface isn’t compatible with the rest of your code. It’s ideal for integrating new classes with old ones or with 3rd-party libraries.

Adapter Pattern: Pros and Cons

Pros:

  • Single Responsibility Principle: Separates the interface conversion code from the primary business logic.

  • Open/Closed Principle: Allows introducing new types of adapters without breaking existing client code.

Cons:

  • Increases overall complexity due to the introduction of new interfaces and classes. Sometimes changing the service class to match the rest of your code is simpler.

Bridge Design Pattern Overview

The Bridge pattern is a structural design pattern that decouples an abstraction from its implementation, allowing the two to vary independently. It’s particularly useful in scenarios where class hierarchies can expand in several dimensions.

Bridge Design Pattern: The Problem

Complex class hierarchies can arise when trying to extend classes in multiple dimensions. For example, adding new shapes and colors to a geometric shape class can exponentially increase the number of required subclasses.

classDiagram
    class Shape {
      +draw()
    }
    class Circle extends Shape
    class Square extends Shape
    class RedCircle extends Circle
    class BlueSquare extends Square

Bridge Design Pattern: Bridge Solution

The Bridge pattern addresses this problem by separating the hierarchy into two distinct layers: abstraction and implementation. This way, you can extend classes in each layer independently.

classDiagram
    class Color {
      +applyColor()
    }
    class Shape {
      Color color
      +draw()
    }
    class Circle extends Shape
    class Square extends Shape
    class Red implements Color
    class Blue implements Color

Bridge Design Pattern: Understanding Abstraction and Implementation

Abstraction (the high-level control layer) delegates the work to the implementation layer (the platform). It’s not about abstract classes or interfaces but the concept of controlling an entity at a higher level.

abstract class Shape {
    protected Color color;
    abstract void draw();
}

Bridge Design Pattern: Real-World Application

Consider an app with different GUIs and support for multiple APIs. The Bridge pattern can prevent a “spaghetti code” scenario by cleanly separating the GUI layer (abstraction) from the API layer (implementation).

classDiagram
    class GUI
    class API
    GUI "1" -- "1" API : uses

Bridge Design Pattern: Bridge Structure

  1. Abstraction: Provides high-level control logic.

  2. Implementation: Declares the common interface for all concrete implementations.

  3. Refined Abstractions: Variants of control logic.

  4. Concrete Implementations: Platform-specific code.

classDiagram
    class Abstraction {
      Implementation impl
      void operation()
    }
    class RefinedAbstraction extends Abstraction
    class Implementation
    class ConcreteImplementationA extends Implementation
    class ConcreteImplementationB extends Implementation

Bridge Design Pattern: Pseudocode Example

Let’s look at an example involving devices and their remote controls, where Device acts as the implementation and Remote as the abstraction.

// Abstraction
class RemoteControl {
    protected Device device;
    RemoteControl(Device device) {
        this.device = device;
    }
    void togglePower() {
        if (device.isEnabled()) {
            device.disable();
        } else {
            device.enable();
        }
    }
}

// Refined Abstraction
class AdvancedRemoteControl extends RemoteControl {
    void mute() {
        device.setVolume(0);
    }
}

Bridge Design Pattern: When to Use the Bridge

  • When you want to avoid a permanent binding between an abstraction and its implementation.

  • When both the abstractions and their implementations should be extensible by subclassing.

  • When changes in the implementation of an abstraction should not impact clients.

Bridge Design Pattern: Implementation Tips

  1. Identify the orthogonal dimensions in your classes.

  2. Define the operations the client needs in the base abstraction class.

  3. Create concrete implementation classes for all platforms, ensuring they follow the implementation interface.

Bridge Design Pattern: Pros and Cons

Pros:

  • Platform independence and flexibility.

  • Client code works with high-level abstractions, not platform details.

  • Adheres to the Open/Closed Principle and Single Responsibility Principle.

Cons:

  • Can complicate the code structure if not well-organized.

ntroduction to Composite Pattern

The Composite pattern is a structural design pattern allowing you to compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions uniformly.

Composite Pattern: Intent of Composite

  • Intent: The Composite pattern aims to treat individual objects and compositions of objects uniformly. It enables recursive tree structures, simplifying client interaction with complex structures.

Composite Pattern: Problem Statement

Problem: In applications representing part-whole hierarchies, like graphical interfaces or file systems, managing individual objects and compositions can be complex without a uniform structure.

Composite Pattern: Solution Overview

Solution: Composite pattern suggests using a common interface for both simple and complex objects, allowing clients to interact with all elements uniformly, simplifying the control over complex structures.

Composite Pattern: Structure of Composite Pattern

classDiagram
    class Component {
      <<interface>>
      +operation()
    }
    class Leaf {
      +operation()
    }
    class Composite {
      +operation()
      +add(Component)
      +remove(Component)
      +getChild(int)
    }
    Component <|-- Leaf
    Component <|-- Composite
    Composite "0..*" --> "0..*" Component

Explanation: The structure includes a Component interface, Leaf representing simple elements, and Composite for complex elements holding children.

Composite Pattern: Key Components

  1. Component: Common interface for all objects in the composition.

  2. Leaf: Represents end objects of a composition.

  3. Composite: A component having children. It stores components and implements their operations.

Composite Pattern: Java Implementation - Component Interface

public interface Graphic {
    void move(int x, int y);
    void draw();
}

Explanation: The Graphic interface defines operations that can be performed on both simple and composite objects.

Composite Pattern: Java Implementation - Leaf Class

public class Dot implements Graphic {
    private int x, y;

    public Dot(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public void move(int x, int y) {
        this.x += x;
        this.y += y;
    }

    public void draw() {
        // Draw the dot at (x, y).
    }
}

Explanation: Dot represents a simple element in the graphic composition, implementing the Graphic interface.

Composite Pattern: Java Implementation - Composite Class

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

public class CompoundGraphic implements Graphic {
    private List<Graphic> children = new ArrayList<>();

    public void add(Graphic child) {
        children.add(child);
    }

    public void remove(Graphic child) {
        children.remove(child);
    }

    public void move(int x, int y) {
        for (Graphic child : children) {
            child.move(x, y);
        }
    }

    public void draw() {
        for (Graphic child : children) {
            child.draw();
        }
        // Draw the complete composition.
    }
}

Explanation: CompoundGraphic acts as a composite, capable of holding and managing child Graphic objects, both simple and complex.

Composite Pattern: Applicability & Benefits

  • Applicability: Use the Composite pattern when you need to implement a tree-like object structure.
  • Benefits: Simplifies client code by allowing it to interact with both simple and complex elements uniformly, using polymorphism and recursion to your advantage.

Introduction to the Decorator Pattern

The Decorator Pattern is a structural design pattern allowing dynamic addition of behaviors to objects without altering their structure. It’s also known as the Wrapper pattern, emphasizing its ability to wrap additional behaviors around objects.

Decorator Pattern: Intent

  • Attach new behaviors to objects dynamically.
  • Provide a flexible alternative to subclassing for extending functionality.

Decorator Pattern: Understanding the Problem

Imagine a notification system initially designed to send emails. As demands grow, there’s a need to add SMS, Facebook, and Slack notifications without bloating the code with subclasses.

Decorator Pattern: Issues with Subclassing

  • Combinatorial explosion of subclasses.
  • Inflexibility to combine multiple behaviors dynamically.

Decorator Pattern: The Decorator Solution

The Decorator Pattern offers a flexible solution by using composition instead of inheritance, allowing objects to gain new behaviors at runtime.

How It Works

  • Wraps the original object inside objects containing new behaviors.

  • Each wrapper (decorator) adds its behavior either before or after delegating to the wrapped object.

Decorator Pattern: Decorator Pattern Structure

Understanding the structural components is crucial for implementing the Decorator Pattern.

classDiagram
    class Component {
        <<interface>>
        operation()
    }
    class ConcreteComponent {
        operation()
    }
    class Decorator {
        operation()
        Component component
    }
    class ConcreteDecoratorA {
        operation()
    }
    class ConcreteDecoratorB {
        operation()
    }

    Component <|-- ConcreteComponent
    Component <|-- Decorator
    Decorator <|-- ConcreteDecoratorA
    Decorator <|-- ConcreteDecoratorB
    Decorator o-- Component : decorates
  • Component: Common interface for both wrappers and wrapped objects.

  • Concrete Component: The object being wrapped.

  • Decorator: Base class for all decorators with a reference to a Component.

  • Concrete Decorators: Classes that add new behaviors.

Decorator Pattern: Implementing the Decorator Pattern

Implementing the Decorator Pattern involves creating a series of decorators that can dynamically add behaviors to the component.

  • Steps for Implementation

    1. Define the component interface.

    2. Create a concrete component class.

    3. Develop a base decorator class.

    4. Implement concrete decorators.

Decorator Pattern: Key Considerations

  • Ensure all components and decorators implement the component interface.

  • Decorators should delegate to the wrapped object and add their behavior.

Java Example - Base Component and Decorator

// Component Interface
interface DataSource {
    void writeData(String data);
    String readData();
}

// Concrete Component
class FileDataSource implements DataSource {
    // Implementation details...
}

// Base Decorator
class DataSourceDecorator implements DataSource {
    protected DataSource wrappee;

    DataSourceDecorator(DataSource source) {
        this.wrappee = source;
    }

    public void writeData(String data) {
        wrappee.writeData(data);
    }

    public String readData() {
        return wrappee.readData();
    }
}
  • The base decorator delegates all work to the wrapped component.

Decorator Pattern: Java Example - Concrete Decorators

// Encryption Decorator
class EncryptionDecorator extends DataSourceDecorator {
    EncryptionDecorator(DataSource source) {
        super(source);
    }

    public void writeData(String data) {
        // Encrypt and write data
    }

    public String readData() {
        // Read and decrypt data
        return "decrypted data";
    }
}

// Compression Decorator
class CompressionDecorator extends DataSourceDecorator {
    CompressionDecorator(DataSource source) {
        super(source);
    }

    public void writeData(String data) {
        // Compress and write data
    }

    public String readData() {
        // Read and decompress data
        return "decompressed data";
    }
}
  • Each decorator adds its behavior either before or after calling the wrapped object.

Decorator Pattern: Using the Decorator Pattern

The Decorator Pattern allows stacking multiple decorators to add several behaviors.

  • Dynamic Behavior Addition
DataSource basicData = new FileDataSource("data.txt");
DataSource encrypted = new EncryptionDecorator(basicData);
DataSource encryptedCompressed = new CompressionDecorator(encrypted);

// Now 'encryptedCompressed' has both encryption and compression capabilities.
  • The client code can combine decorators in various configurations at runtime.

Decorator Pattern: Applicability and Benefits

  • When to Use the Decorator Pattern

    • When you need to add responsibilities to objects dynamically.

    • When subclassing is impractical due to a large number of behaviors.

Decorator Pattern: Advantages

  • Greater flexibility than static inheritance.

  • Avoids feature-laden classes with multiple behaviors.

  • Supports Single Responsibility Principle by allowing functionalities to be divided into separate classes.

Decorator Pattern: Considerations and Conclusion

  • Considerations

    • Overuse can lead to complex designs.

    • Specific decorator order might be required for certain behaviors.

  • Conclusion

    The Decorator Pattern provides a flexible approach to add new behaviors to objects dynamically, promoting code reusability and extension without modifying existing code. Understanding its structure and implementation is key to effectively enhancing object functionalities.

Introduction to Facade Pattern

The Facade pattern is a structural design pattern that provides a simplified interface to a complex subsystem, making it easier to use. It’s particularly useful when working with a large set of objects that belong to a sophisticated library or framework.

Facade Pattern: Intent of Facade

Facade’s main intent is to provide a simplified interface to a complex subsystem, encapsulating the complexities and providing a more accessible and straightforward way for clients to interact with the system.

classDiagram
    class Facade {
      +simplifiedInterface()
    }
    Facade --> SubsystemClass1 : delegates calls
    Facade --> SubsystemClass2 : delegates calls

Facade Pattern: Problem Addressed by Facade

Complex subsystems often require in-depth knowledge about the interrelationships and interactions between objects. Facade addresses the issue by providing a single simplified interface, reducing the learning curve and minimizing client-side dependencies.

Facade Pattern: Facade Solution

A facade class offers limited functionality in comparison to working directly with the subsystem but includes only those features that clients really care about, making the subsystem easier to use and integrate.

Facade Pattern: Real-World Analogy

Consider an operator in a shop as a facade. They provide you with a simple voice interface to various services and departments of the shop, hiding the complexities of the subsystems behind the scenes.

Facade Pattern: Structure of Facade Pattern

classDiagram
    class Facade {
      +simpleMethod()
    }
    class ComplexSubsystemClass1 {
      +complexMethodA()
    }
    class ComplexSubsystemClass2 {
      +complexMethodB()
    }
    Facade --> ComplexSubsystemClass1
    Facade --> ComplexSubsystemClass2

The Facade provides a simple method that delegates client requests to appropriate subsystem objects, managing their interactions.

Facade Pattern: Facade Implementation in Java

// Facade Class
public class VideoConverter {
    public File convert(String filename, String format) {
        VideoFile file = new VideoFile(filename);
        Codec sourceCodec = new CodecFactory().extract(file);
        Codec destinationCodec;
        if (format.equals("mp4")) {
            destinationCodec = new MPEG4CompressionCodec();
        } else {
            destinationCodec = new OggCompressionCodec();
        }
        String buffer = BitrateReader.read(filename, sourceCodec);
        String result = BitrateReader.convert(buffer, destinationCodec);
        return new AudioMixer().fix(result);
    }
}

// Client Code
public class Application {
    public static void main(String[] args) {
        VideoConverter converter = new VideoConverter();
        File mp4 = converter.convert("funny-cats-video.ogg", "mp4");
        mp4.save();
    }
}

Facade Pattern: Applicability of Facade

Use the Facade pattern when you have a complex subsystem and want to provide a simple interface to it, or when you want to structure subsystems into layers, using facades to communicate between them.

Facade Pattern: Benefits and Drawbacks

  • Pros: Simplifies the interface to complex subsystems and isolates clients from subsystem components.
  • Cons: A facade can become a god object coupled to all classes of an app.

Facade Pattern: Relations with Other Patterns

  • Adapter vs. Facade: Adapter wraps one object, while Facade works with an entire subsystem of objects.
  • Facade and Singleton: Often, a single facade object is sufficient, making it a good candidate for a Singleton.

Flyweight Pattern: Introduction to Flyweight

The Flyweight is a structural design pattern focused on efficient data sharing. It allows for managing a large number of objects with minimal memory usage by sharing common parts of the state.

Flyweight Pattern: Understanding the Problem

The issue arises when a system requires a vast number of similar objects, leading to high memory consumption. This is common in graphics-heavy applications, like games, where each object like bullets or particles stores all its data.

Flyweight Pattern: Flyweight Solution

The Flyweight pattern addresses this by separating the object’s state into intrinsic (shared) and extrinsic (unique) parts. Only the intrinsic state is shared among objects, significantly reducing memory requirements.

Flyweight Pattern: Intrinsic and Extrinsic State

classDiagram
  class Flyweight {
    -IntrinsicState intrinsic
    +Operation(extrinsicState)
  }
  class Context {
    -ExtrinsicState extrinsic
  }
  Flyweight <-- Context: uses

Intrinsic State: Shared and immutable.

Extrinsic State: Unique, passed to the flyweight’s methods.

Flyweight Pattern: Flyweight in Action

Consider a game with millions of trees, each with its position and type. By applying Flyweight, each tree shares common data (like type) and only stores its unique position.

Flyweight Pattern: Implementing Flyweight in Java

// Flyweight class
class TreeType {
    private String name;
    private String color;
    private String texture;
    TreeType(String name, String color, String texture) {
        this.name = name;
        this.color = color;
        this.texture = texture;
    }
    void draw(String canvas, int x, int y) {
        // Render the tree on the canvas at x, y
    }
}

// Context class
class Tree {
    private int x, y;
    private TreeType type;
    Tree(int x, int y, TreeType type) {
        this.x = x;
        this.y = y;
        this.type = type;
    }
    void draw(String canvas) {
        type.draw(canvas, this.x, this.y);
    }
}

Flyweight Pattern: Flyweight Factory

classDiagram
  class FlyweightFactory {
    -FlyweightPool pool
    +getFlyweight(intrinsicState)
  }
  class FlyweightPool {
    -Collection flyweights
  }
  Flyweight <-- FlyweightFactory: creates/returns
  FlyweightPool <-- FlyweightFactory: manages

A factory manages the flyweights to ensure that each intrinsic state is only created once.

Flyweight Pattern: Benefits of Flyweight

  1. Reduced Memory Usage: By sharing common data, it significantly lowers memory footprint.

  2. Improved Performance: Less memory consumption means faster access and less load.

Flyweight Pattern: Considerations

  • Complexity: The pattern introduces additional layers, increasing the system’s complexity.

  • Immutability: Flyweights should be immutable to ensure safe sharing across contexts.

Flyweight Pattern: Real-World Example and Summary

Imagine an online game with various character models. Using Flyweight for shared aspects like model and textures, while individual characters have unique states like health and position, optimizes memory usage and performance.

Introduction to Proxy Pattern

The Proxy pattern is a structural design pattern that provides a substitute or placeholder for another object. It controls access to the original object, allowing actions to be performed before or after the request reaches the object.

Proxy Pattern: Intent of Proxy

Intent: To provide a surrogate or placeholder for another object to control access to it. This is useful in situations where you want to add a layer of complexity or control without changing the object itself.

Proxy Pattern: The Problem

Proxy solves issues like managing a massive object that consumes vast system resources. It implements lazy initialization, only creating the object when needed, thus optimizing resource usage and performance.

Proxy Pattern: The Solution

classDiagram
    class ServiceInterface {
        <<interface>>
        +request()
    }
    class RealService {
        +request()
    }
    class Proxy {
        -realService: RealService
        +request()
    }
    ServiceInterface <|-- RealService
    ServiceInterface <|-- Proxy

The Proxy pattern suggests creating a new proxy class with the same interface as the original service object. The proxy handles requests and delegates the work to the real service object when necessary.

Proxy Pattern: Real-World Analogy

A real-world analogy for the Proxy pattern is a credit card acting as a proxy for a bank account, which in turn is a proxy for a bundle of cash. Both provide a means for payment but with additional layers of control and convenience.

Proxy Pattern: Structure of the Proxy Pattern

classDiagram
    class ServiceInterface {
        +serviceMethod()
    }
    class Service {
        +serviceMethod()
    }
    class Proxy {
        -service: Service
        +serviceMethod()
    }
    Client --|> ServiceInterface
    ServiceInterface <|-- Service
    ServiceInterface <|-- Proxy

The structure includes a ServiceInterface that both the RealService and Proxy implement. The Proxy contains a reference to Service and controls access to it.

Proxy Pattern: Proxy in Action - Java Example

// The service interface
interface ThirdPartyYouTubeLib {
    void listVideos();
    void getVideoInfo(String id);
    void downloadVideo(String id);
}

// The real service class that communicates with YouTube
class ThirdPartyYouTubeClass implements ThirdPartyYouTubeLib {
    public void listVideos() { /*...*/ }
    public void getVideoInfo(String id) { /*...*/ }
    public void downloadVideo(String id) { /*...*/ }
}

// The proxy class implementing the same interface
class CachedYouTubeClass implements ThirdPartyYouTubeLib {
    private ThirdPartyYouTubeLib service;
    private Object listCache, videoCache;
    private boolean needReset = false;

    public CachedYouTubeClass(ThirdPartyYouTubeLib service) {
        this.service = service;
    }

    public void listVideos() {
        if (listCache == null || needReset) {
            listCache = service.listVideos();
        }
    }

    public void getVideoInfo(String id) {
        if (videoCache == null || needReset) {
            videoCache = service.getVideoInfo(id);
        }
    }

    public void downloadVideo(String id) {
        // implementation for caching and downloading
    }
}

This Java code demonstrates a Proxy (CachedYouTubeClass) for a YouTube service, implementing caching to optimize performance.

Proxy Pattern: Applicability of Proxy

Proxy is versatile and can be used for: - Lazy initialization (virtual proxy) - Access control (protection proxy) - Local execution of a remote service (remote proxy) - Logging requests (logging proxy) - Caching request results (caching proxy) - Smart reference (counting references to objects)

Proxy Pattern: Implementation Steps

  1. Define a service interface.
  2. Create a proxy class implementing the service interface.
  3. In the proxy, add a reference to the service.
  4. Implement proxy methods to include additional behaviors.
  5. Optionally, implement lazy initialization and other enhancements.

Proxy Pattern: Pros and Cons

Pros:

  • Control the service object indirectly.

  • Manage the lifecycle and initialization of the service.

  • Introduce new proxies without changing the service or clients.

Cons:

  • Can complicate the code structure with additional classes.

  • May introduce latency in the response from the service.

Behavioral Design Patterns

Introduction to Chain of Responsibility

The Chain of Responsibility (CoR) is a behavioral design pattern that allows passing requests along a chain of handlers. Each handler decides whether to process the request or pass it to the next handler in the chain.

CoR: The Problem

Imagine an online ordering system needing to process various checks like authentication and data validation sequentially. As more checks get added, the code becomes bloated and hard to maintain, making the system rigid and complex.

CoR: The Solution

CoR addresses this by turning each check into a handler and linking them into a chain. Each handler has the opportunity to process the request and decide whether to pass it further along the chain.

CoR: Structure of CoR - Part 1

classDiagram
  class Handler {
    -Handler next
    +handle(request)
  }
  class ConcreteHandler1 {
    +handle(request)
  }
  class ConcreteHandler2 {
    +handle(request)
  }
  Handler <|-- ConcreteHandler1
  Handler <|-- ConcreteHandler2

Handlers are linked in a chain, each capable of processing the request or passing it along.

CoR: Structure of CoR - Part 2

  • Handler: The interface common for all handlers.

  • Concrete Handlers: Implement the actual processing and decide whether to pass the request along the chain.

  • Client: Composes the chain and can trigger processing from any handler in the chain.

CoR: Implementation in Java - Handler Interface

public interface Handler {
    void setNext(Handler handler);
    void handle(Request request);
}

Defines the method for setting the next handler and the method to process requests.

CoR: Implementation in Java - Concrete Handlers

public class ConcreteHandler1 implements Handler {
    private Handler next;
    
    public void setNext(Handler handler) {
        next = handler;
    }
    
    public void handle(Request request) {
        if (canHandle(request)) {
            // Process request
        } else if (next != null) {
            next.handle(request);
        }
    }
}

Each handler decides whether to process the request or pass it to the next.

CoR: Real-World Analogy

A call to tech support goes through multiple operators. Each operator is a handler that either resolves your issue or escalates it to the next level.

CoR: Applicability

Use CoR when: - Your program expects to process different kinds of requests in various ways.

  • It’s essential to execute handlers in a particular order.

  • The set of handlers and their order might change at runtime.

CoR: Pros and Cons

Pros:

  • Decouples the sender and receivers of a request.

  • You can control the order of request handling.

  • Adheres to Single Responsibility and Open/Closed Principles.

Cons: - Some requests may end up unhandled.

Introduction to Command Pattern

The Command pattern is a behavioral design pattern that turns a request into a stand-alone object containing all information about the request. This encapsulation allows for more flexible and extensible request handling, such as queuing, logging, and undoing actions.

Command Design Pattern: The Problem

Creating a new text-editor app with a toolbar and various buttons, each performing different operations, presents a design challenge. The naive approach would lead to a proliferation of subclasses for each button’s action, making the system hard to maintain and extend.

Command Design Pattern: The Command Pattern Solution

The Command pattern addresses this by separating the request for an action from its execution. A command object encapsulates a request as an object, thereby allowing for parameterization of clients with queues, requests, and operations.

classDiagram
    class Command {
      <<interface>>
      execute()
    }
    class ConcreteCommand {
      execute()
    }
    class Invoker {
      command: Command
    }
    class Receiver {
      action()
    }
    Command <|.. ConcreteCommand
    ConcreteCommand --> Receiver : calls action on
    Invoker --> Command : stores

Command Design Pattern: Structure of the Command Pattern

The Command pattern typically includes several components: Sender (Invoker), Command, ConcreteCommand, Receiver, and Client.

  • Sender (Invoker): Initiates the request.

  • Command: Interface for executing commands.

  • ConcreteCommand: Implements Command, defining the binding between a Receiver and an action.

  • Receiver: Contains the business logic to perform the action.

  • Client: Creates a ConcreteCommand and sets its receiver.

Command Design Pattern: Java Example - Command Interface

// The Command interface with a single execute method
public interface Command {
    void execute();
}

Command Design Pattern: Java Example - ConcreteCommand

// A ConcreteCommand to perform a specific action
public class LightOnCommand implements Command {
    // Reference to the receiver
    private Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    public void execute() {
        light.on();
    }
}

Command Design Pattern: Java Example - Receiver

// The Receiver class
public class Light {
    public void on() {
        System.out.println("Light is on!");
    }

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

Command Design Pattern: Java Example - Invoker

// The Invoker class
public class RemoteControl {
    private Command command;

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

    public void pressButton() {
        command.execute();
    }
}

Command Design Pattern: Applicability of the Command Pattern

  • Parametrization: Use the Command pattern to parameterize objects with operations.

  • Queue or Schedule Operations: Suitable for queuing, scheduling, or logging operations.

  • Implementing Undo/Redo: Command can keep a history of executed commands and state, allowing for undoing and redoing actions.

Command Design Pattern: Pros and Cons

  • Pros:

    • Adheres to the Single Responsibility and Open/Closed Principles.

    • Enables implementing undo/redo and deferred execution of operations.

    • Allows for assembling complex commands from simple ones.

  • Cons:

    • Can increase complexity by adding additional layers between senders and receivers.

Introduction to Iterator Pattern

The Iterator is a behavioral design pattern that allows sequential traversal through a collection without exposing its underlying representation (like lists, stacks, trees, etc.).

Iterator Pattern: Problem Statement

Collections are common but vary widely (lists, trees, graphs). Providing a uniform way to access their elements simplifies code and makes it more adaptable.

Iterator Pattern: Solution Overview

The Iterator pattern extracts the traversal behavior into a separate object, the iterator, which encapsulates traversal details and allows multiple simultaneous traversals.

classDiagram
    class Iterator {
      <<interface>>
      +first()
      +next()
      +isDone()
      +currentItem()
    }
    class ConcreteIterator {
      +first()
      +next()
      +isDone()
      +currentItem()
    }
    class Aggregate {
      <<interface>>
      +createIterator()
    }
    class ConcreteAggregate {
      +createIterator()
    }
    Iterator <|.. ConcreteIterator
    Aggregate <|.. ConcreteAggregate
    ConcreteAggregate o-- ConcreteIterator

Iterator Pattern: Real-World Analogy

Navigating a city like Rome with various tools: your own exploration, a smartphone app, or a local guide. Each method is akin to an iterator, offering a way to explore the city (collection) effectively.

Iterator Pattern: Structure of the Pattern

  • Iterator Interface: Declares operations for traversing a collection.

  • Concrete Iterator: Implements traversal for a specific collection.

  • Collection Interface: Declares methods for getting iterators compatible with the collection.

  • Concrete Collection: Provides specific iterators.

  • Client: Works with both collections and iterators via interfaces.

Iterator Pattern: Java Implementation: Interfaces

// The collection interface declares a factory method for producing iterators.
interface SocialNetwork {
    ProfileIterator createFriendsIterator(String profileId);
    ProfileIterator createCoworkersIterator(String profileId);
}

// The common interface for all iterators.
interface ProfileIterator {
    Profile getNext();
    boolean hasMore();
}

Iterator Pattern: Java Implementation: Concrete Classes

// Concrete collection class
class Facebook implements SocialNetwork {
    public ProfileIterator createFriendsIterator(String profileId) {
        return new FacebookIterator(this, profileId, "friends");
    }

    // ... other methods ...
}

// Concrete iterator class
class FacebookIterator implements ProfileIterator {
    private Facebook facebook;
    private String profileId;
    private int currentPosition;

    // ... constructor and methods ...
}

Iterator Pattern: Applicability

Use the Iterator when:

  • Your collection has a complex structure and you want to hide its complexity.

  • You want to reduce duplication of traversal code.

  • Your code needs to traverse different structures or unknown structures at runtime.

Iterator Pattern: How to Implement

  1. Declare Iterator Interface: Define methods for fetching the next element, checking end of iteration, etc.

  2. Declare Collection Interface: Describe methods for fetching iterators.

  3. Implement Concrete Iterators and Collections: Link iterator with collection instance, usually in the iterator’s constructor.

  4. Update Client: Replace collection traversal with iterators.

Iterator Pattern: Pros and Cons

  • Pros:

    • Supports the Single Responsibility and Open/Closed principles.

    • Allows parallel iteration and delayed iteration.

  • Cons:

    • Can be overkill for simple collections.

    • May be less efficient than direct traversal.

Iterator Pattern: Relations with Other Patterns

  • Composite: Use Iterators to traverse Composite trees.

  • Factory Method: Collection subclasses can return different types of iterators.

  • Memento: Capture iteration state and roll back if necessary.

  • Visitor: Combine with Iterator to perform operations over element classes.

Introduction to Mediator Pattern

The Mediator is a behavioral design pattern that reduces chaotic dependencies between objects. It centralizes communication by forcing objects to collaborate through a mediator object, simplifying their relationships and making them easier to manage.

Mediator Pattern: Problem Statement

In complex systems, components often interact in a decentralized manner, leading to a web of direct dependencies. For example, in a UI dialog, various elements like buttons, text fields, and checkboxes might interact directly, creating a tightly coupled system that’s hard to maintain and extend.

Mediator Pattern: Mediator Solution

The Mediator pattern suggests stopping direct communication between components and instead, using a mediator object for all interactions. This centralizes control and decouples the components from each other, making the system easier to maintain and extend.

classDiagram
    class Mediator {
      <<interface>>
      +notify(sender: Component, event: string)
    }
    class ConcreteMediator {
      -title: string
      -components: Component[]
      +notify(sender: Component, event: string)
    }
    Mediator <|.. ConcreteMediator

Mediator Pattern: Real-World Analogy

Just like an air traffic control tower manages the planes’ landing and takeoff to prevent accidents, the Mediator pattern manages communication between components to prevent them from direct interdependencies and chaos.

Mediator Pattern: Mediator Structure

  1. Components: Classes with business logic, each having a reference to the mediator.

  2. Mediator Interface: Declares the method of communication, typically a notification method.

  3. Concrete Mediator: Implements the mediator interface and coordinates communication.

classDiagram
    class Component {
        -mediator: Mediator
        +notify(event: string)
    }
    class ConcreteComponent1 {
        +notify(event: string)
    }
    class ConcreteComponent2 {
        +notify(event: string)
    }
    Component <|-- ConcreteComponent1
    Component <|-- ConcreteComponent2
    Component "1" -- "1" Mediator : communicates >

Slide 6: Mediator in Action - Java Example

// Mediator Interface
interface Mediator {
    void notify(Component sender, String event);
}

// Concrete Mediator
class AuthenticationDialog implements Mediator {
    private Title title;
    private Checkbox loginOrRegisterChkBx;
    // Other components...

    public void notify(Component sender, String event) {
        if (sender == loginOrRegisterChkBx && event.equals("check")) {
            // Handle the check event
        }
        // Other event handling
    }
}

// Components
class Component {
    Mediator dialog;

    void click() {
        dialog.notify(this, "click");
    }

    void check() {
        dialog.notify(this, "check");
    }
    // Other component interactions...
}

Mediator Pattern: Applicability of Mediator

Use the Mediator when:

  • You want to reduce the coupling between various components of a program.

  • You find yourself creating tons of component subclasses to reuse basic behavior in various contexts.

  • You need to extract all relationships between classes into a separate class.

Mediator Pattern: Implementing the Mediator

  1. Identify tightly coupled classes and envision how a mediator can manage their interactions.

  2. Declare the mediator interface with a notification method.

  3. Implement a concrete mediator to manage the components.

  4. Components should reference the mediator and use it for communication.

  5. Redirect all inter-component communications to the mediator.

Slide 9: Pros and Cons

Pros:

  • Simplifies maintenance by centralizing control.

  • Encourages reusability and adherence to the Single Responsibility and Open/Closed Principles.

Cons:

  • The mediator can become a God Object if not managed carefully.

Mediator Pattern: Relations with Other Patterns

  • Chain of Responsibility: Passes a request along a chain of potential handlers.

  • Observer: Establishes dynamic one-way connections between objects, often used within the Mediator pattern.

  • Facade: Organizes collaboration between classes like a Mediator but doesn’t centralize communication.

Memento Pattern

  • The Memento pattern is a behavioral design pattern that allows saving and restoring the previous state of an object without revealing the details of its implementation.

  • Also known as: Snapshot

Memento Pattern: Intent

Memento is a behavioral design pattern that lets you save and restore the previous state of an object without revealing the details of its implementation.

Memento Pattern: Problem

Consider a text editor with complex functionalities. Implementing an “undo” feature requires saving the state of objects before operations. Directly accessing objects’ private states to produce snapshots can violate encapsulation and make the system fragile.

Memento Pattern: Solution

The Memento pattern addresses these issues by delegating the creation of state snapshots to the object itself, the originator. It encapsulates the state in a memento object that’s accessible only to the originator, while other objects interact with a limited interface.

Memento Pattern: Structure - Classic Implementation

classDiagram
    class Originator {
      +createSnapshot() Snapshot
      +restore(state) void
    }
    class Memento {
      -state
    }
    class Caretaker {
      -mementos Stack
    }
    Originator --> Memento: creates >
    Caretaker --> Memento: holds >

Originator can create and restore snapshots of its state, while Caretaker holds the history.

Memento Pattern: Memento Class in Java

class Memento {
    private String state;

    public Memento(String state) {
        this.state = state;
    }

    public String getState() {
        return state;
    }
}

The Memento class encapsulates the state of the Originator.

Memento Pattern: Originator Class in Java

class Originator {
    private String state;

    public void set(String state) {
        this.state = state;
    }

    public Memento saveToMemento() {
        return new Memento(state);
    }

    public void restoreFromMemento(Memento memento) {
        state = memento.getState();
    }
}

Originator uses Memento to save and restore its internal state.

Memento Pattern: Caretaker Role

The Caretaker holds mementos without modifying them, managing the timeline of states. It triggers the originator to create new mementos or restore an earlier state from a memento.

Memento Pattern: Use Cases

  • Undo Mechanisms: Like in text editors or drawing applications.
  • Transaction Rollback: In databases or systems requiring rollback capabilities.

The Memento pattern is powerful for any scenario requiring a return to a previous state.

Memento Pattern: Implementation Considerations

  1. Determine the Originator: Identify the class whose state needs to be saved.
  2. Immutable Mementos: Mementos should be immutable after creation.
  3. Encapsulation: Ensure that only the originator can modify its internal state.

Memento Pattern: Pros and Cons

Pros: - Encapsulates and preserves object’s state without exposing its structure. - Simplifies the originator’s code by externalizing state history management.

Cons: - Can be memory intensive if mementos are created frequently. - Caretakers need to track the originator’s lifecycle to manage mementos.

Introduction to Observer Pattern

The Observer pattern is a behavioral design pattern that allows an object, known as a subject, to notify other objects about changes in its state. It is also known as Event-Subscriber or Listener.

Observer Pattern: Intent

Observer pattern allows for the establishment of a subscription mechanism to notify multiple objects about any events that happen to the object they’re observing. This ensures that interested objects receive updates efficiently and autonomously.

Observer Pattern: Problem and Solution

  • Problem: Managing knowledge about changes in a system’s state can be complex when multiple entities need updates.

  • Solution: Observer pattern offers a subscription model where subjects notify observers about changes, promoting decoupling and efficient data distribution.

Observer Pattern: Structure of Observer Pattern

classDiagram
    class Subject {
      -observers : Observer[]
      +attach(observer : Observer) : void
      +detach(observer : Observer) : void
      +notify() : void
    }
    class Observer {
      +update(subject : Subject) : void
    }
    Subject "1" --> "*" Observer
  • Subject: Maintains a list of observers and notifies them of changes.

  • Observer: Provides an update interface to receive notifications from the subject.

Observer Pattern: Components

  • Publisher (Subject): The entity whose state changes are of interest and which broadcasts notifications.

  • Subscriber (Observer): Entities that wish to be notified about changes in the publisher’s state.

Observer Pattern: Implementation in Java: Defining the Observer and Subject

// Observer Interface
interface Observer {
    void update(String eventType, File file);
}

// Subject Class
class EventManager {
    Map<String, List<Observer>> listeners = new HashMap<>();

    void subscribe(String eventType, Observer listener) {
        this.listeners.computeIfAbsent(eventType, k -> new ArrayList<>()).add(listener);
    }

    void unsubscribe(String eventType, Observer listener) {
        this.listeners.get(eventType).remove(listener);
    }

    void notify(String eventType, File file) {
        this.listeners.get(eventType).forEach(listener -> listener.update(eventType, file));
    }
}

Observer Pattern: Implementation in Java: Concrete Classes

// Concrete Observer
class LoggingListener implements Observer {
    private String logFilename;
    private String message;

    LoggingListener(String logFilename, String message) {
        this.logFilename = logFilename;
        this.message = message;
    }

    @Override
    public void update(String eventType, File file) {
        System.out.println("Logging to " + logFilename + ": " + message.replace("%s", file.getName()));
    }
}

// Another Concrete Observer...

Observer Pattern: Real-World Analogy

Just like subscribers to a magazine don’t need to check the store for new issues and instead receive updates directly, the Observer pattern lets objects subscribe to and receive updates from other objects, streamlining the information flow and notification process.

Observer Pattern: Applicability

Use the Observer pattern when:

  • The change of state in one object requires changes in others, and the exact number or objects is unknown or dynamic.

  • When you need a robust and maintainable way to handle updates among interconnected objects.

Observer Pattern: Pros and Cons

Pros:

  • Supports the Open/Closed Principle.

  • Establishes relationships between objects at runtime.

Cons:

  • Subscribers are notified in random order, which may not always be desirable.

Introduction to State Design Pattern

The State pattern is a behavioral design pattern that allows an object to alter its behavior when its internal state changes. This pattern is akin to the object changing its class.

State Design Pattern: Intent of State Pattern

State pattern enables an object to behave differently depending on its internal state. It encapsulates the varying behavior for the same routine based on the object’s state, making it appear as if the object has changed its class.

State Design Pattern: Problem and Solution

  • Problem: Managing behavior variations of an object based on its state can lead to complex and hard-to-maintain code structures.

  • Solution: The State pattern suggests encapsulating state-specific behaviors into separate classes and allowing the context to change its behavior by switching between these state objects.

State Design Pattern: Structure of State Pattern

classDiagram
    class Context {
      -state : State
    }
    class State {
      +handle() : void
    }
    class ConcreteStateA {
      +handle() : void
    }
    class ConcreteStateB {
      +handle() : void
    }
    Context --> State : has
    State <|-- ConcreteStateA
    State <|-- ConcreteStateB
  • Context: Maintains an instance of a ConcreteState subclass that defines the current state.

  • State: Defines an interface for encapsulating the behavior associated with a particular state of the Context.

  • Concrete States: Implement various behaviors associated with different states of the Context.

State Design Pattern: Implementation in Java: Defining the Context and State

// Context Class
class AudioPlayer {
    private State state;
    // Methods that change state
    void changeState(State state) {
        this.state = state;
    }
    // Other methods...
}

// State Interface
interface State {
    void clickLock();
    void clickPlay();
    void clickNext();
    void clickPrevious();
}

State Design Pattern: Concrete State Classes

// Concrete States
class ReadyState implements State {
    public void clickPlay() {
        // Handle play button in ready state
    }
    // Other handlers...
}

class LockedState implements State {
    public void clickLock() {
        // Handle lock button in locked state
    }
    // Other handlers...
}

State Design Pattern: Real-World Analogy

Just as a mobile phone’s behavior changes based on its state (locked, unlocked, charging), the State pattern allows an object’s behavior to change at runtime depending on its state, promoting flexibility and cleaner code.

State Design Pattern: Applicability

Use the State pattern when:

  • An object’s behavior changes drastically depending on its state, and you want to avoid massive conditional blocks in your code.

  • The number of states and transitions increase and you want to manage them more efficiently.

State Design Pattern: Pros and Cons

Pros:

  • Organizes state-specific code into separate classes.

  • Introduces new states without changing existing classes, following the Open/Closed Principle.

Cons:

  • Can be overkill for a state machine with only a few states or infrequent changes.

State Design Pattern: Relations with Other Patterns

State is often compared to Strategy, though they address different problems. Both use composition to change the behavior of the context, but State is more dynamic, allowing states to know about each other and initiate transitions.

Introduction to Strategy Design Pattern

The Strategy pattern is a behavioral design pattern that enables selecting an algorithm’s implementation at runtime. It defines a family of algorithms, encapsulates each one, and makes them interchangeable.

Strategy Design Pattern: Intent of Strategy Pattern

The intent of the Strategy pattern is to define a set of interchangeable algorithms or strategies that can be selected at runtime according to the needs of the context or client.

Strategy Design Pattern: Problem and Solution

  • Problem: Need for a flexible way to incorporate different behaviors or algorithms within a class and the ability to change them at runtime.

  • Solution: The Strategy pattern suggests separating the behavior into different strategy classes and using a reference to these strategies in the context class.

Strategy Design Pattern: Structure of Strategy Pattern

classDiagram
    class Context {
      -strategy : Strategy
      +Context(strategy : Strategy)
      +setStrategy(strategy : Strategy) : void
      +executeStrategy() : void
    }
    class Strategy {
      +executeAlgorithm() : void
    }
    class ConcreteStrategyA {
      +executeAlgorithm() : void
    }
    class ConcreteStrategyB {
      +executeAlgorithm() : void
    }
    Context --> Strategy : has
    Strategy <|-- ConcreteStrategyA
    Strategy <|-- ConcreteStrategyB
  • Context: Maintains a reference to a Strategy object and delegates it the algorithm execution.

  • Strategy: Common interface for all strategies defining the algorithm execution method.

  • ConcreteStrategy: Implements the algorithm using the Strategy interface.

Strategy Design Pattern: Implementation in Java: Context and Strategy

// Strategy Interface
interface Strategy {
    void executeAlgorithm();
}

// Context Class
class Context {
    private Strategy strategy;

    Context(Strategy strategy) {
        this.strategy = strategy;
    }

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

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

Strategy Design Pattern: Concrete Strategies in Java

// Concrete Strategy A
class ConcreteStrategyA implements Strategy {
    public void executeAlgorithm() {
        // Implement algorithm A
    }
}

// Concrete Strategy B
class ConcreteStrategyB implements Strategy {
    public void executeAlgorithm() {
        // Implement algorithm B
    }
}

Strategy Design Pattern: Real-World Analogy

Consider various transportation strategies for getting to the airport. You can take a bus, a taxi, or cycle. Depending on the context like time and budget, you switch between these strategies.

Strategy Design Pattern: Applicability

Use the Strategy pattern when:

  • You have different variations of an algorithm and want to switch between them at runtime.

  • You want to avoid exposing complex, algorithm-specific data structures.

  • You want to replace inheritance with composition for behavioral variations.

Strategy Design Pattern: Pros and Cons

Pros: - Enables the Open/Closed Principle by allowing the introduction of new strategies without changing the context.

  • Simplifies unit testing by isolating algorithms.

Cons:

  • Increases the number of objects in the application.

  • Clients must be aware of the differences between strategies to select the right one.

Strategy Design Pattern: Relations with Other Patterns

The Strategy pattern is often used with other patterns like Factory to create the strategies, and it can resemble the Command pattern, but with a focus on algorithms. It also contrasts with Template Method, which instead of composition, uses inheritance to change parts of an algorithm.

Introduction to Template Method Pattern

The Template Method is a behavioral design pattern that defines the skeleton of an algorithm in a base class but lets subclasses override specific steps of the algorithm without changing its structure.

Template Method Pattern: Intent of Template Method

The intent is to let subclasses implement the variable parts of an algorithm while the sequence of steps is defined at one place in the base class, promoting reusability and inversion of control.

Template Method Pattern: Problem and Solution

  • Problem: Algorithms might require different implementations of specific steps, but changing the algorithm’s structure each time is inefficient and error-prone.

  • Solution: The Template Method pattern suggests defining an algorithm’s structure in a base class with concrete implementations in subclasses for the steps that vary.

Template Method Pattern: Structure of Template Method

classDiagram
    class AbstractClass {
        -templateMethod() : void
        -primitiveOperation1() : void
        -primitiveOperation2() : void
    }
    class ConcreteClass {
        -primitiveOperation1() : void
        -primitiveOperation2() : void
    }
    AbstractClass <|-- ConcreteClass : Inherits
  • AbstractClass: Defines template methods and primitive operations.

  • ConcreteClass: Implements the primitive operations to carry out subclass-specific steps of the algorithm.

Template Method Pattern: Implementation in Java: Abstract Class

// Abstract Class
abstract class GameAI {
    // Template method defines the skeleton of an algorithm.
    final void turn() {
        collectResources();
        buildStructures();
        buildUnits();
        attack();
    }

    void collectResources() { /* default implementation */ }
    abstract void buildStructures();
    abstract void buildUnits();
    abstract void attack();
}

Template Method Pattern: Concrete Class in Java

// Concrete Class
class OrcsAI extends GameAI {
    void buildStructures() {
        // Specific implementation for Orcs
    }

    void buildUnits() {
        // Specific implementation for Orcs
    }

    void attack() {
        // Specific implementation for Orcs
    }
}

Template Method Pattern: Real-World Analogy

The Template Method is like a standard architectural plan for a building where certain parts can be altered to fit the client’s specific needs without changing the overall structure.

Template Method Pattern: Applicability

Use the Template Method pattern when you want to:

  • Let clients extend only particular steps of an algorithm, not the whole algorithm or its structure.

  • Eliminate code duplication by pulling up shared steps into a single superclass.

Template Method Pattern: Pros and Cons

Pros:

  • Facilitates code reuse and the principle of inversion of control.

  • Allows subclasses to extend the algorithm without changing its structure.

Cons:

  • Can limit the flexibility by requiring the algorithm’s structure to remain constant.

  • May lead to a more complicated design due to the requirement of abstract classes.

Template Method Pattern: Relations with Other Patterns

  • Factory Method: Often a step in the Template Method.

  • Strategy: Similar to Template Method, but uses composition to change parts of the algorithm, providing more flexibility.

Introduction to Visitor Design Pattern

The Visitor pattern is a behavioral design pattern that allows adding new behaviors to existing class hierarchies without altering any existing code. It helps in separating an algorithm from the object structure it operates on.

Visitor Design Pattern: Intent

The intent of the Visitor pattern is to encapsulate an operation that you want to perform on the elements of a data structure. It allows you to define new operations without changing the classes of the elements you operate on.

Visitor Design Pattern: Problem and Solution

  • Problem: Extending functionalities of complex class hierarchies without modifying the existing code can be cumbersome and invasive.

  • Solution: The Visitor pattern suggests moving the new behavior into a separate class called a visitor, so the original object structure can remain intact and new functions can be added easily.

Visitor Design Pattern: Structure of Visitor Pattern

classDiagram
    class Visitor {
        +visitConcreteElementA(ConcreteElementA) : void
        +visitConcreteElementB(ConcreteElementB) : void
    }
    class ConcreteVisitor1 {
        +visitConcreteElementA(ConcreteElementA) : void
        +visitConcreteElementB(ConcreteElementB) : void
    }
    class ConcreteVisitor2 {
        +visitConcreteElementA(ConcreteElementA) : void
        +visitConcreteElementB(ConcreteElementB) : void
    }
    class Element {
        +accept(visitor : Visitor) : void
    }
    class ConcreteElementA {
        +accept(visitor : Visitor) : void
    }
    class ConcreteElementB {
        +accept(visitor : Visitor) : void
    }
    Visitor <|-- ConcreteVisitor1
    Visitor <|-- ConcreteVisitor2
    Element <|-- ConcreteElementA
    Element <|-- ConcreteElementB
    Element "1" --> "n" Visitor : is visited by
  • Visitor: Interface declaring visit operations for each element class.

  • ConcreteVisitor: Implements visit operations for each element class.

  • Element: Interface declaring the accept method.

  • ConcreteElement: Implements an accept method that takes a visitor object.

Visitor Design Pattern: Implementation in Java: Element and Visitor Interfaces

// Element Interface
interface Element {
    void accept(Visitor visitor);
}

// Concrete Element A
class ConcreteElementA implements Element {
    void accept(Visitor visitor) {
        visitor.visitConcreteElementA(this);
    }
    // Other methods...
}

// Visitor Interface
interface Visitor {
    void visitConcreteElementA(ConcreteElementA element);
    void visitConcreteElementB(ConcreteElementB element);
}

Visitor Design Pattern: Concrete Visitor in Java

// Concrete Visitor
class ConcreteVisitor1 implements Visitor {
    public void visitConcreteElementA(ConcreteElementA element) {
        // Implement specific behavior for ConcreteElementA
    }

    public void visitConcreteElementB(ConcreteElementB element) {
        // Implement specific behavior for ConcreteElementB
    }
}

Visitor Design Pattern: Real-World Analogy

A good insurance agent offers different policies to various types of organizations. Similarly, the Visitor pattern lets you execute different operations on objects of various classes by having a visitor object implement several variants of the same operation.

Visitor Design Pattern: Applicability

Use the Visitor pattern when:

  • You need to perform an operation across a set of objects with different classes.

  • You want to keep related operations together by defining them in one class.

  • You need to add functions to complex class hierarchies but want to avoid recompiling the code or changing the existing classes.

Visitor Design Pattern: Pros and Cons

Pros:

  • Adheres to the Open/Closed Principle by allowing the introduction of new operations without modifying the classes.

  • Groups related operations together, making the system more organized.

Cons:

  • Adding a new ConcreteElement class requires changing all Visitor classes.

  • Visitors might lack access to private fields and methods of the elements they work with, potentially compromising encapsulation.

Visitor Design Pattern: Relations with Other Patterns

  • Composite: Often used with Visitor to traverse complex structures.

  • Command: Visitor can be regarded as a powerful version of Command pattern with operations over objects of different classes.

  • Iterator: Combined with Visitor, it helps traverse complex structures and perform operations over its elements.

Cover In-Depth

Behavioral patterns: 1. Observer 2. Command 3. Template 4. Iterator 5. State

Structural patterns: 1. Decorator 2. Adapter 3. Facade 4. Composite 5. Proxy

Creational patterns 1. Factory Method 2. Singleton

Compound Pattern