Gang of Four Design Patterns in Java: Complete Guide (2025)


Gang of Four Design Patterns in Java

Design patterns are reusable solutions to common problems in software design. The Gang of Four (GoF) patterns, introduced in the book "Design Patterns: Elements of Reusable Object-Oriented Software" by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, are considered fundamental to good object-oriented design. This comprehensive guide explores all 23 GoF patterns with practical Java examples.

Pro Tip: Understanding and applying GoF design patterns leads to more maintainable, flexible, and reusable code.

1. Introduction to Design Patterns

Note: Design patterns are not ready-made code solutions but rather templates for solving common design problems.

Design patterns are proven solutions to recurring design problems in software development. They provide a common vocabulary for developers and help create more maintainable, flexible, and reusable code. The Gang of Four (GoF) patterns are categorized into three main groups:

  • Creational Patterns: Deal with object creation mechanisms
  • Structural Patterns: Deal with object composition and relationships
  • Behavioral Patterns: Deal with object interaction and responsibility distribution

Understanding these patterns helps developers:

  • Write more maintainable code
  • Solve common design problems efficiently
  • Communicate design solutions effectively
  • Create flexible and reusable software

2. Creational Patterns

Note: Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.

Creational patterns provide various object creation mechanisms, which increase flexibility and reuse of existing code. They help make a system independent of how its objects are created, composed, and represented. The five creational patterns are:

  • Singleton Pattern
  • Factory Method Pattern
  • Abstract Factory Pattern
  • Builder Pattern
  • Prototype Pattern

2.1 Singleton Pattern

Pro Tip: Use the Singleton pattern when you need exactly one instance of a class, and you need to provide a global point of access to it.

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This pattern is particularly useful when exactly one object is needed to coordinate actions across the system.

When to Use Singleton Pattern

  • When you need to control access to a shared resource
  • When you need to maintain a single point of control
  • When you need to coordinate actions across the system
  • When you need to maintain a single instance of a class

Implementation in Java

Here's how to implement a thread-safe Singleton pattern in Java:


public class Singleton {
    // Private static instance
    private static volatile Singleton instance;
    
    // Private constructor to prevent instantiation
    private Singleton() {
        // Prevent instantiation via reflection
        if (instance != null) {
            throw new IllegalStateException("Singleton already initialized");
        }
    }
    
    // Public static method to get the instance
    public static Singleton getInstance() {
        // Double-checked locking pattern
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    
    // Example method
    public void doSomething() {
        System.out.println("Singleton instance is doing something");
    }
}

Enum Singleton Implementation

Here's how to implement a Singleton pattern using enum in Java, which is the most recommended approach:


public enum EnumSingleton {
    INSTANCE;
    
    // Example method
    public void doSomething() {
        System.out.println("Enum Singleton instance is doing something");
    }
}

The enum singleton approach provides several advantages:

  • Thread safety is guaranteed by the JVM
  • Serialization is handled automatically
  • Reflection attacks are prevented
  • Code is more concise and readable

Usage Example


public class SingletonDemo {
    public static void main(String[] args) {
        // Get the singleton instance
        Singleton singleton = Singleton.getInstance();
        
        // Use the singleton
        singleton.doSomething();
        
        // Try to create another instance
        // This will throw an exception
        try {
            Singleton anotherInstance = new Singleton();
        } catch (IllegalStateException e) {
            System.out.println("Cannot create another instance: " + e.getMessage());
        }
    }
}

Best Practices

  • Use lazy initialization for better performance
  • Implement double-checked locking for thread safety
  • Prevent reflection attacks by checking instance in constructor
  • Consider using enum for simpler implementation
  • Document the singleton nature of the class

Common Pitfalls

  • Not handling thread safety properly
  • Creating multiple instances through reflection
  • Using singletons for everything (overuse)
  • Making the singleton too complex
  • Not considering serialization issues

Real-World Applications

  • Database connection pools
  • Configuration managers
  • Logging systems
  • Cache implementations
  • Thread pools
Note: While the Singleton pattern is useful, it should be used judiciously as it can make code harder to test and maintain if overused.

2.2 Factory Method Pattern

Pro Tip: Use the Factory Method pattern when you need to create objects without specifying their exact classes.

The Factory Method pattern defines an interface for creating an object, but lets subclasses decide which class to instantiate. This pattern lets a class defer instantiation to subclasses.

When to Use Factory Method Pattern

  • When a class can't anticipate the class of objects it must create
  • When a class wants its subclasses to specify the objects it creates
  • When classes delegate responsibility to one of several helper subclasses
  • When you want to localize knowledge of which class gets created

Implementation in Java

Here's how to implement the Factory Method pattern in Java:


// Product interface
interface Document {
    void open();
    void save();
    void close();
}

// Concrete products
class PDFDocument implements Document {
    @Override
    public void open() {
        System.out.println("Opening PDF document");
    }
    
    @Override
    public void save() {
        System.out.println("Saving PDF document");
    }
    
    @Override
    public void close() {
        System.out.println("Closing PDF document");
    }
}

class WordDocument implements Document {
    @Override
    public void open() {
        System.out.println("Opening Word document");
    }
    
    @Override
    public void save() {
        System.out.println("Saving Word document");
    }
    
    @Override
    public void close() {
        System.out.println("Closing Word document");
    }
}

// Creator abstract class
abstract class DocumentCreator {
    // Factory method
    public abstract Document createDocument();
    
    // Template method
    public void processDocument() {
        Document doc = createDocument();
        doc.open();
        doc.save();
        doc.close();
    }
}

// Concrete creators
class PDFDocumentCreator extends DocumentCreator {
    @Override
    public Document createDocument() {
        return new PDFDocument();
    }
}

class WordDocumentCreator extends DocumentCreator {
    @Override
    public Document createDocument() {
        return new WordDocument();
    }
}

Usage Example


public class FactoryMethodDemo {
    public static void main(String[] args) {
        // Create PDF document
        DocumentCreator pdfCreator = new PDFDocumentCreator();
        pdfCreator.processDocument();
        
        // Create Word document
        DocumentCreator wordCreator = new WordDocumentCreator();
        wordCreator.processDocument();
    }
}

Best Practices

  • Use abstract classes or interfaces for the creator
  • Keep the factory method simple and focused
  • Consider using parameterized factory methods
  • Document the purpose of each factory method
  • Use meaningful names for factory methods

Common Pitfalls

  • Creating too many factory methods
  • Making factory methods too complex
  • Not properly documenting factory methods
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • Document processing systems
  • GUI frameworks
  • Database connection factories
  • Logging frameworks
  • Plugin architectures
Note: The Factory Method pattern is often used in frameworks where the framework needs to create objects but doesn't know their exact types.

2.3 Abstract Factory Pattern

Pro Tip: Use the Abstract Factory pattern when you need to create families of related objects without specifying their concrete classes.

The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. This pattern is particularly useful when a system needs to be independent of how its products are created, composed, and represented.

When to Use Abstract Factory Pattern

  • When a system needs to be independent of how its products are created
  • When a system needs to be configured with multiple families of products
  • When a family of related product objects is designed to be used together
  • When you want to provide a class library of products

Implementation in Java

Here's how to implement the Abstract Factory pattern in Java:


// Abstract Product A
interface Button {
    void render();
    void onClick();
}

// Abstract Product B
interface Checkbox {
    void render();
    void onCheck();
}

// Concrete Product A1
class WindowsButton implements Button {
    @Override
    public void render() {
        System.out.println("Rendering Windows button");
    }
    
    @Override
    public void onClick() {
        System.out.println("Windows button clicked");
    }
}

// Concrete Product A2
class MacButton implements Button {
    @Override
    public void render() {
        System.out.println("Rendering Mac button");
    }
    
    @Override
    public void onClick() {
        System.out.println("Mac button clicked");
    }
}

// Concrete Product B1
class WindowsCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("Rendering Windows checkbox");
    }
    
    @Override
    public void onCheck() {
        System.out.println("Windows checkbox checked");
    }
}

// Concrete Product B2
class MacCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("Rendering Mac checkbox");
    }
    
    @Override
    public void onCheck() {
        System.out.println("Mac checkbox checked");
    }
}

// Abstract Factory
interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

// Concrete Factory 1
class WindowsFactory implements GUIFactory {
    @Override
    public Button createButton() {
        return new WindowsButton();
    }
    
    @Override
    public Checkbox createCheckbox() {
        return new WindowsCheckbox();
    }
}

// Concrete Factory 2
class MacFactory implements GUIFactory {
    @Override
    public Button createButton() {
        return new MacButton();
    }
    
    @Override
    public Checkbox createCheckbox() {
        return new MacCheckbox();
    }
}

// Client
class Application {
    private Button button;
    private Checkbox checkbox;
    
    public Application(GUIFactory factory) {
        button = factory.createButton();
        checkbox = factory.createCheckbox();
    }
    
    public void render() {
        button.render();
        checkbox.render();
    }
}

Usage Example


public class AbstractFactoryDemo {
    public static void main(String[] args) {
        // Create Windows UI
        GUIFactory windowsFactory = new WindowsFactory();
        Application windowsApp = new Application(windowsFactory);
        windowsApp.render();
        
        // Create Mac UI
        GUIFactory macFactory = new MacFactory();
        Application macApp = new Application(macFactory);
        macApp.render();
    }
}

Best Practices

  • Use interfaces for abstract factories and products
  • Keep the factory methods focused and simple
  • Consider using dependency injection
  • Document the relationships between products
  • Use meaningful names for factories and products

Common Pitfalls

  • Creating too many product families
  • Making the factory too complex
  • Not properly documenting product relationships
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • Cross-platform UI frameworks
  • Database access layers
  • Plugin systems
  • Configuration management
  • Testing frameworks
Note: The Abstract Factory pattern is particularly useful when you need to ensure that a set of related objects are used together.

2.4 Builder Pattern

Pro Tip: Use the Builder pattern when you need to create complex objects with many possible configurations.

The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations.

When to Use Builder Pattern

  • When an object needs to be created with many optional components
  • When the construction process should be separated from the representation
  • When you need to create different representations of the same object
  • When you want to make the construction process more flexible

Implementation in Java

Here's how to implement the Builder pattern in Java:


// Product class
class Computer {
    private String CPU;
    private String RAM;
    private String storage;
    private String GPU;
    
    private Computer(ComputerBuilder builder) {
        this.CPU = builder.CPU;
        this.RAM = builder.RAM;
        this.storage = builder.storage;
        this.GPU = builder.GPU;
    }
    
    // Getters
    public String getCPU() { return CPU; }
    public String getRAM() { return RAM; }
    public String getStorage() { return storage; }
    public String getGPU() { return GPU; }
    
    // Builder class
    public static class ComputerBuilder {
        private String CPU;
        private String RAM;
        private String storage;
        private String GPU;
        
        public ComputerBuilder setCPU(String CPU) {
            this.CPU = CPU;
            return this;
        }
        
        public ComputerBuilder setRAM(String RAM) {
            this.RAM = RAM;
            return this;
        }
        
        public ComputerBuilder setStorage(String storage) {
            this.storage = storage;
            return this;
        }
        
        public ComputerBuilder setGPU(String GPU) {
            this.GPU = GPU;
            return this;
        }
        
        public Computer build() {
            return new Computer(this);
        }
    }
}

Usage Example


public class BuilderDemo {
    public static void main(String[] args) {
        // Create a gaming computer
        Computer gamingComputer = new Computer.ComputerBuilder()
            .setCPU("Intel i9")
            .setRAM("32GB")
            .setStorage("1TB SSD")
            .setGPU("NVIDIA RTX 3080")
            .build();
            
        // Create a budget computer
        Computer budgetComputer = new Computer.ComputerBuilder()
            .setCPU("Intel i3")
            .setRAM("8GB")
            .setStorage("256GB SSD")
            .build();
    }
}

Best Practices

  • Make the builder class static and nested
  • Use method chaining for fluent interface
  • Validate parameters in the build method
  • Make the product class immutable
  • Use meaningful names for builder methods

Common Pitfalls

  • Not making the product class immutable
  • Not validating parameters
  • Making the builder too complex
  • Not using method chaining
  • Not documenting required parameters

Real-World Applications

  • Configuration builders
  • Query builders
  • Document builders
  • UI component builders
  • Test data builders
Note: The Builder pattern is particularly useful when you need to create complex objects with many optional parameters.

2.5 Prototype Pattern

Pro Tip: Use the Prototype pattern when you need to create objects without knowing their exact classes.

The Prototype pattern specifies the kinds of objects to create using a prototypical instance, and creates new objects by copying this prototype.

When to Use Prototype Pattern

  • When a system should be independent of how its products are created
  • When the class of objects to create is specified at run-time
  • When instances of a class can have one of only a few different combinations of state
  • When you want to avoid building a class hierarchy of factories

Implementation in Java

Here's how to implement the Prototype pattern in Java:


// Prototype interface
interface Shape extends Cloneable {
    Shape clone();
    void draw();
}

// Concrete prototype 1
class Circle implements Shape {
    private int radius;
    
    public Circle(int radius) {
        this.radius = radius;
    }
    
    @Override
    public Shape clone() {
        try {
            return (Shape) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing circle with radius: " + radius);
    }
}

// Concrete prototype 2
class Rectangle implements Shape {
    private int width;
    private int height;
    
    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }
    
    @Override
    public Shape clone() {
        try {
            return (Shape) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing rectangle with width: " + width + " and height: " + height);
    }
}

// Prototype registry
class ShapeRegistry {
    private static Map shapes = new HashMap<>();
    
    static {
        shapes.put("circle", new Circle(10));
        shapes.put("rectangle", new Rectangle(20, 30));
    }
    
    public static Shape getShape(String type) {
        return shapes.get(type).clone();
    }
}

Usage Example


public class PrototypeDemo {
    public static void main(String[] args) {
        // Get a circle from the registry
        Shape circle = ShapeRegistry.getShape("circle");
        circle.draw();
        
        // Get a rectangle from the registry
        Shape rectangle = ShapeRegistry.getShape("rectangle");
        rectangle.draw();
    }
}

Best Practices

  • Implement Cloneable interface
  • Use deep copy when necessary
  • Consider using a prototype registry
  • Document clone behavior
  • Handle CloneNotSupportedException

Common Pitfalls

  • Not implementing deep copy when needed
  • Not handling CloneNotSupportedException
  • Making the prototype too complex
  • Not documenting clone behavior
  • Not considering thread safety

Real-World Applications

  • GUI frameworks
  • Game development
  • Document processing
  • Configuration management
  • Database access
Note: The Prototype pattern is particularly useful when you need to create objects without knowing their exact classes.

3. Structural Patterns

Note: Structural patterns deal with object composition and relationships between objects.

Structural patterns help you compose classes and objects into larger structures while keeping these structures flexible and efficient. They focus on how classes and objects are composed to form larger structures. The seven structural patterns are:

  • Adapter Pattern
  • Bridge Pattern
  • Composite Pattern
  • Decorator Pattern
  • Facade Pattern
  • Flyweight Pattern
  • Proxy Pattern

3.1 Adapter Pattern

Pro Tip: Use the Adapter pattern when you need to make existing classes work with others without modifying their source code.

The Adapter pattern converts the interface of a class into another interface that clients expect. It lets classes work together that couldn't otherwise because of incompatible interfaces.

When to Use Adapter Pattern

  • When you want to use an existing class, but its interface doesn't match the one you need
  • When you want to create a reusable class that cooperates with unrelated classes
  • When you need to use several existing subclasses, but it's impractical to adapt their interface by subclassing each one
  • When you need to provide a stable interface to similar components with different interfaces

Implementation in Java

Here's how to implement the Adapter pattern in Java:


// Target interface
interface MediaPlayer {
    void play(String audioType, String fileName);
}

// Adaptee class
class AdvancedMediaPlayer {
    public void playVlc(String fileName) {
        System.out.println("Playing vlc file: " + fileName);
    }
    
    public void playMp4(String fileName) {
        System.out.println("Playing mp4 file: " + fileName);
    }
}

// Adapter class
class MediaAdapter implements MediaPlayer {
    private AdvancedMediaPlayer advancedMusicPlayer;
    
    public MediaAdapter(String audioType) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMusicPlayer = new AdvancedMediaPlayer();
        } else if (audioType.equalsIgnoreCase("mp4")) {
            advancedMusicPlayer = new AdvancedMediaPlayer();
        }
    }
    
    @Override
    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMusicPlayer.playVlc(fileName);
        } else if (audioType.equalsIgnoreCase("mp4")) {
            advancedMusicPlayer.playMp4(fileName);
        }
    }
}

// Client class
class AudioPlayer implements MediaPlayer {
    private MediaAdapter mediaAdapter;
    
    @Override
    public void play(String audioType, String fileName) {
        // Inbuilt support for mp3
        if (audioType.equalsIgnoreCase("mp3")) {
            System.out.println("Playing mp3 file: " + fileName);
        }
        // MediaAdapter provides support for other formats
        else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
            mediaAdapter = new MediaAdapter(audioType);
            mediaAdapter.play(audioType, fileName);
        } else {
            System.out.println("Invalid media type: " + audioType);
        }
    }
}

Usage Example


public class AdapterDemo {
    public static void main(String[] args) {
        AudioPlayer audioPlayer = new AudioPlayer();
        
        // Play mp3 file
        audioPlayer.play("mp3", "song.mp3");
        
        // Play vlc file
        audioPlayer.play("vlc", "movie.vlc");
        
        // Play mp4 file
        audioPlayer.play("mp4", "video.mp4");
        
        // Try to play unsupported format
        audioPlayer.play("avi", "movie.avi");
    }
}

Best Practices

  • Use interfaces for the target and adaptee
  • Keep the adapter focused and simple
  • Consider using object composition over inheritance
  • Document the purpose of the adapter
  • Use meaningful names for adapter classes

Common Pitfalls

  • Making the adapter too complex
  • Not properly documenting the adapter's purpose
  • Using inheritance when composition would be better
  • Creating too many adapters
  • Not considering thread safety

Real-World Applications

  • Legacy system integration
  • Third-party library integration
  • Database drivers
  • GUI frameworks
  • Web service clients
Note: The Adapter pattern is particularly useful when you need to integrate new code with existing code without modifying the existing code.

3.2 Bridge Pattern

Pro Tip: Use the Bridge pattern when you want to separate an abstraction from its implementation so that both can vary independently.

The Bridge pattern decouples an abstraction from its implementation so that the two can vary independently. It's particularly useful when you want to avoid a permanent binding between an abstraction and its implementation.

When to Use Bridge Pattern

  • When you want to avoid a permanent binding between an abstraction and its implementation
  • When both abstractions and implementations should be extensible by subclassing
  • When changes in the implementation of an abstraction should have no impact on clients
  • When you want to share an implementation among multiple objects

Implementation in Java

Here's how to implement the Bridge pattern in Java:


// Implementor interface
interface DrawingAPI {
    void drawCircle(double x, double y, double radius);
    void drawRectangle(double x, double y, double width, double height);
}

// Concrete Implementor 1
class DrawingAPI1 implements DrawingAPI {
    @Override
    public void drawCircle(double x, double y, double radius) {
        System.out.printf("API1.circle at %.2f:%.2f radius %.2f%n", x, y, radius);
    }
    
    @Override
    public void drawRectangle(double x, double y, double width, double height) {
        System.out.printf("API1.rectangle at %.2f:%.2f width %.2f height %.2f%n", x, y, width, height);
    }
}

// Concrete Implementor 2
class DrawingAPI2 implements DrawingAPI {
    @Override
    public void drawCircle(double x, double y, double radius) {
        System.out.printf("API2.circle at %.2f:%.2f radius %.2f%n", x, y, radius);
    }
    
    @Override
    public void drawRectangle(double x, double y, double width, double height) {
        System.out.printf("API2.rectangle at %.2f:%.2f width %.2f height %.2f%n", x, y, width, height);
    }
}

// Abstraction
abstract class Shape {
    protected DrawingAPI drawingAPI;
    
    protected Shape(DrawingAPI drawingAPI) {
        this.drawingAPI = drawingAPI;
    }
    
    public abstract void draw();
    public abstract void resizeByPercentage(double pct);
}

// Refined Abstraction 1
class CircleShape extends Shape {
    private double x, y, radius;
    
    public CircleShape(double x, double y, double radius, DrawingAPI drawingAPI) {
        super(drawingAPI);
        this.x = x;
        this.y = y;
        this.radius = radius;
    }
    
    @Override
    public void draw() {
        drawingAPI.drawCircle(x, y, radius);
    }
    
    @Override
    public void resizeByPercentage(double pct) {
        radius *= (1.0 + pct/100.0);
    }
}

// Refined Abstraction 2
class RectangleShape extends Shape {
    private double x, y, width, height;
    
    public RectangleShape(double x, double y, double width, double height, DrawingAPI drawingAPI) {
        super(drawingAPI);
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }
    
    @Override
    public void draw() {
        drawingAPI.drawRectangle(x, y, width, height);
    }
    
    @Override
    public void resizeByPercentage(double pct) {
        width *= (1.0 + pct/100.0);
        height *= (1.0 + pct/100.0);
    }
}

Usage Example


public class BridgeDemo {
    public static void main(String[] args) {
        DrawingAPI api1 = new DrawingAPI1();
        DrawingAPI api2 = new DrawingAPI2();
        
        // Create shapes with different implementations
        Shape[] shapes = {
            new CircleShape(1, 2, 3, api1),
            new CircleShape(5, 7, 11, api2),
            new RectangleShape(1, 2, 3, 4, api1),
            new RectangleShape(5, 7, 11, 13, api2)
        };
        
        // Draw and resize all shapes
        for (Shape shape : shapes) {
            shape.draw();
            shape.resizeByPercentage(50);
            shape.draw();
        }
    }
}

Best Practices

  • Use interfaces for the implementor
  • Keep the abstraction and implementation separate
  • Consider using dependency injection
  • Document the bridge relationship
  • Use meaningful names for bridge components

Common Pitfalls

  • Making the bridge too complex
  • Not properly documenting the bridge relationship
  • Creating too many bridge classes
  • Not considering thread safety
  • Not properly handling state

Real-World Applications

  • GUI frameworks
  • Database drivers
  • Device drivers
  • Plugin architectures
  • Cross-platform applications
Note: The Bridge pattern is particularly useful when you need to separate an abstraction from its implementation so that both can vary independently.

3.3 Composite Pattern

Pro Tip: Use the Composite pattern when you want to compose objects into tree structures to represent part-whole hierarchies.

The Composite pattern lets clients treat individual objects and compositions of objects uniformly. It's particularly useful when you want to represent part-whole hierarchies.

When to Use Composite Pattern

  • When you want to compose objects into tree structures to represent part-whole hierarchies
  • When you want to represent part-whole hierarchies in a flat structure
  • When you want to treat individual objects and compositions of objects uniformly
  • When you want to implement a recursive structure

Implementation in Java

Here's how to implement the Composite pattern in Java:


// Component interface
interface Component {
    void add(Component component);
    void remove(Component component);
    void display();
}

// Leaf
class Leaf implements Component {
    private String name;
    
    public Leaf(String name) {
        this.name = name;
    }
    
    @Override
    public void add(Component component) {
        // Leaf doesn't add children
    }
    
    @Override
    public void remove(Component component) {
        // Leaf doesn't remove children
    }
    
    @Override
    public void display() {
        System.out.println(name);
    }
}

// Composite
class Composite implements Component {
    private List children = new ArrayList<>();
    
    @Override
    public void add(Component component) {
        children.add(component);
    }
    
    @Override
    public void remove(Component component) {
        children.remove(component);
    }
    
    @Override
    public void display() {
        for (Component component : children) {
            component.display();
        }
    }
}

Usage Example


public class CompositeDemo {
    public static void main(String[] args) {
        Component leaf1 = new Leaf("Leaf 1");
        Component leaf2 = new Leaf("Leaf 2");
        Component leaf3 = new Leaf("Leaf 3");
        
        Composite composite = new Composite();
        composite.add(leaf1);
        composite.add(leaf2);
        
        Composite composite2 = new Composite();
        composite2.add(leaf3);
        composite2.add(composite);
        
        composite2.display();
    }
}

Best Practices

  • Use interfaces for the component
  • Keep the composite simple and focused
  • Consider using dependency injection
  • Document the composite relationship
  • Use meaningful names for components

Common Pitfalls

  • Creating too many components
  • Making the composite too complex
  • Not properly documenting the composite relationship
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • GUI frameworks
  • File systems
  • Database access layers
  • Component architectures
  • Tree structures
Note: The Composite pattern is particularly useful when you want to compose objects into tree structures to represent part-whole hierarchies.

3.4 Decorator Pattern

Pro Tip: Use the Decorator pattern when you want to add new behaviors or responsibilities to objects dynamically without changing their source code.

The Decorator pattern attaches additional responsibilities to an object dynamically. It's particularly useful when you want to add new behaviors or responsibilities to objects dynamically without changing their source code.

When to Use Decorator Pattern

  • When you want to add new behaviors or responsibilities to objects dynamically
  • When you want to avoid a permanent binding between an object and its behaviors
  • When you want to implement a flexible alternative to subclassing
  • When you want to add responsibilities to individual objects

Implementation in Java

Here's how to implement the Decorator pattern in Java:


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

// Concrete component
class SimpleCoffee implements Coffee {
    @Override
    public double getCost() {
        return 5.0;
    }
    
    @Override
    public String getDescription() {
        return "Simple Coffee";
    }
}

// Decorator abstract class
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;
    
    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }
    
    @Override
    public double getCost() {
        return decoratedCoffee.getCost();
    }
    
    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription();
    }
}

// Concrete decorator 1
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }
    
    @Override
    public double getCost() {
        return super.getCost() + 1.0;
    }
    
    @Override
    public String getDescription() {
        return super.getDescription() + ", Milk";
    }
}

// Concrete decorator 2
class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }
    
    @Override
    public double getCost() {
        return super.getCost() + 0.5;
    }
    
    @Override
    public String getDescription() {
        return super.getDescription() + ", Sugar";
    }
}

Usage Example


public class DecoratorDemo {
    public static void main(String[] args) {
        Coffee simpleCoffee = new SimpleCoffee();
        System.out.println(simpleCoffee.getDescription() + " - $" + simpleCoffee.getCost());
        
        Coffee milkCoffee = new MilkDecorator(simpleCoffee);
        System.out.println(milkCoffee.getDescription() + " - $" + milkCoffee.getCost());
        
        Coffee sugarMilkCoffee = new SugarDecorator(milkCoffee);
        System.out.println(sugarMilkCoffee.getDescription() + " - $" + sugarMilkCoffee.getCost());
    }
}

Best Practices

  • Use interfaces for the component and decorator
  • Keep the decorator simple and focused
  • Consider using dependency injection
  • Document the decorator relationship
  • Use meaningful names for decorators

Common Pitfalls

  • Creating too many decorators
  • Making the decorator too complex
  • Not properly documenting the decorator relationship
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • GUI frameworks
  • Component architectures
  • Logging frameworks
  • Data processing
  • Query processors
Note: The Decorator pattern is particularly useful when you want to add new behaviors or responsibilities to objects dynamically without changing their source code.

3.5 Facade Pattern

Pro Tip: Use the Facade pattern when you want to provide a simplified interface to a complex subsystem.

The Facade pattern provides a unified interface to a set of interfaces in a subsystem. It defines a higher-level interface that makes the subsystem easier to use.

When to Use Facade Pattern

  • When you want to provide a simplified interface to a complex subsystem
  • When you want to hide the complexities of a subsystem
  • When you want to isolate clients from components of a subsystem
  • When you want to simplify the interface of a subsystem

Implementation in Java

Here's how to implement the Facade pattern in Java:


// Subsystem classes
class CPU {
    public void freeze() {
        System.out.println("CPU is freezing");
    }
    public void jump(long position) {
        System.out.println("CPU is jumping to " + position);
    }
    public void execute() {
        System.out.println("CPU is executing");
    }
}

class Memory {
    public void load(long position, byte[] data) {
        System.out.println("Memory loaded " + data.length + " bytes at position " + position);
    }
}

class HardDrive {
    public byte[] read(long lba, int size) {
        System.out.println("HardDrive read " + size + " bytes from LBA " + lba);
        return new byte[size];
    }
}

// Facade
class Computer {
    private CPU cpu;
    private Memory memory;
    private HardDrive hardDrive;
    
    public Computer() {
        this.cpu = new CPU();
        this.memory = new Memory();
        this.hardDrive = new HardDrive();
    }
    
    public void start() {
        cpu.freeze();
        memory.load(0, hardDrive.read(0, 1024));
        cpu.jump(0);
        cpu.execute();
    }
}

Usage Example


public class FacadeDemo {
    public static void main(String[] args) {
        AbstractClass abstractClass = new ConcreteClass();
        abstractClass.templateMethod();
    }
}

Best Practices

  • Use abstract classes for the template
  • Keep the template simple and focused
  • Consider using dependency injection
  • Document the template
  • Use meaningful names for template methods

Common Pitfalls

  • Creating too many templates
  • Making the template too complex
  • Not properly documenting the template
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • Algorithm implementation
  • Data processing
  • Query processors
  • GUI frameworks
  • Chat systems
Note: The Template Method pattern is particularly useful when you want to let subclasses implement different steps of an algorithm without changing the algorithm's structure.

3.6 Flyweight Pattern

Pro Tip: Use the Flyweight pattern when you want to share objects to support large numbers of fine-grained objects efficiently.

The Flyweight pattern shares objects to support large numbers of fine-grained objects efficiently.

When to Use Flyweight Pattern

  • When you want to share objects to support large numbers of fine-grained objects efficiently
  • When you want to reduce the number of objects in a system
  • When you want to localize extrinsic state
  • When you want to improve performance

Implementation in Java

Here's how to implement the Flyweight pattern in Java:


// Flyweight interface
interface Shape {
    void draw();
}

// Concrete flyweight 1
class CircleShape implements Shape {
    private String color;
    
    public CircleShape(String color) {
        this.color = color;
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing circle with color: " + color);
    }
}

// Concrete flyweight 2
class RectangleShape implements Shape {
    private String color;
    
    public RectangleShape(String color) {
        this.color = color;
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing rectangle with color: " + color);
    }
}

// Flyweight factory
class ShapeFactory {
    private static final Map shapes = new HashMap<>();
    
    public static Shape getShape(String color) {
        return shapes.computeIfAbsent(color, c -> {
            if (c.equalsIgnoreCase("red")) {
                return new CircleShape(c);
            } else if (c.equalsIgnoreCase("blue")) {
                return new RectangleShape(c);
            } else {
                throw new IllegalArgumentException("Unsupported shape color");
            }
        });
    }
}

Usage Example


public class FlyweightDemo {
    public static void main(String[] args) {
        Shape redCircle = ShapeFactory.getShape("red");
        Shape blueRectangle = ShapeFactory.getShape("blue");
        
        redCircle.draw();
        blueRectangle.draw();
    }
}

Best Practices

  • Use interfaces for the flyweight
  • Keep the flyweight simple and focused
  • Consider using dependency injection
  • Document the flyweight
  • Use meaningful names for flyweights

Common Pitfalls

  • Creating too many flyweights
  • Making the flyweight too complex
  • Not properly documenting the flyweight
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • GUI frameworks
  • Game development
  • Data processing
  • Query processors
  • Algorithm implementation
Note: The Flyweight pattern is particularly useful when you want to share objects to support large numbers of fine-grained objects efficiently.

3.7 Proxy Pattern

Pro Tip: Use the Proxy pattern when you want to provide a surrogate or placeholder for another object to control access to it.

The Proxy pattern provides a surrogate or placeholder for another object to control access to it.

When to Use Proxy Pattern

  • When you want to provide a surrogate or placeholder for another object to control access to it
  • When you want to add additional functionality to an object without changing its interface
  • When you want to control access to an object
  • When you want to provide a local representation of a remote object

Implementation in Java

Here's how to implement the Proxy pattern in Java:


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

// Real subject
class RealImage implements Image {
    private String fileName;
    
    public RealImage(String fileName) {
        this.fileName = fileName;
        loadImageFromDisk();
    }
    
    private void loadImageFromDisk() {
        System.out.println("Loading " + fileName);
    }
    
    @Override
    public void display() {
        System.out.println("Displaying " + fileName);
    }
}

// Proxy
class ProxyImage implements Image {
    private String fileName;
    private RealImage realImage;
    
    public ProxyImage(String fileName) {
        this.fileName = fileName;
    }
    
    @Override
    public void display() {
        if (realImage == null) {
            realImage = new RealImage(fileName);
        }
        realImage.display();
    }
}

Usage Example


public class ProxyDemo {
    public static void main(String[] args) {
        Image image1 = new ProxyImage("image1.jpg");
        Image image2 = new ProxyImage("image2.jpg");
        
        image1.display();
        image2.display();
    }
}

Best Practices

  • Use interfaces for the subject and proxy
  • Keep the proxy simple and focused
  • Consider using dependency injection
  • Document the proxy
  • Use meaningful names for proxies

Common Pitfalls

  • Creating too many proxies
  • Making the proxy too complex
  • Not properly documenting the proxy
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • Remote access
  • Access control
  • Lazy loading
  • Caching
  • Logging
Note: The Proxy pattern is particularly useful when you want to provide a surrogate or placeholder for another object to control access to it.

4. Behavioral Patterns

Note: Behavioral patterns deal with object interaction and responsibility distribution.

Behavioral patterns deal with object interaction and responsibility distribution. They focus on how classes and objects are composed to form larger structures. The behavioral patterns are:

  • Chain of Responsibility Pattern
  • Command Pattern
  • Interpreter Pattern
  • Iterator Pattern
  • Mediator Pattern
  • Memento Pattern
  • Observer Pattern
  • State Pattern
  • Strategy Pattern
  • Template Method Pattern
  • Visitor Pattern

4.1 Chain of Responsibility Pattern

Pro Tip: Use the Chain of Responsibility pattern when you want to give more than one object a chance to handle a request.

The Chain of Responsibility pattern gives more than one object a chance to handle a request. It avoids coupling the sender of a request to its receiver by giving more than one object a chance to handle the request.

When to Use Chain of Responsibility Pattern

  • When you want to give more than one object a chance to handle a request
  • When you want to avoid coupling the sender of a request to its receiver
  • When you want to implement a request processing pipeline
  • When you want to implement a loosely coupled system

Implementation in Java

Here's how to implement the Chain of Responsibility pattern in Java:


// Handler interface
interface Handler {
    void handleRequest(String request);
}

// Concrete handler 1
class ConcreteHandler1 implements Handler {
    private Handler nextHandler;
    
    @Override
    public void handleRequest(String request) {
        if (request.equals("Request1")) {
            System.out.println("ConcreteHandler1 handled the request");
        } else if (nextHandler != null) {
            nextHandler.handleRequest(request);
        }
    }
    
    public void setNextHandler(Handler nextHandler) {
        this.nextHandler = nextHandler;
    }
}

// Concrete handler 2
class ConcreteHandler2 implements Handler {
    private Handler nextHandler;
    
    @Override
    public void handleRequest(String request) {
        if (request.equals("Request2")) {
            System.out.println("ConcreteHandler2 handled the request");
        } else if (nextHandler != null) {
            nextHandler.handleRequest(request);
        }
    }
    
    public void setNextHandler(Handler nextHandler) {
        this.nextHandler = nextHandler;
    }
}

Usage Example


public class ChainOfResponsibilityDemo {
    public static void main(String[] args) {
        Handler handler1 = new ConcreteHandler1();
        Handler handler2 = new ConcreteHandler2();
        
        handler1.setNextHandler(handler2);
        
        handler1.handleRequest("Request1");
        handler1.handleRequest("Request2");
    }
}

Best Practices

  • Use interfaces for the handler
  • Keep the chain simple and focused
  • Consider using dependency injection
  • Document the chain
  • Use meaningful names for handlers

Common Pitfalls

  • Creating too many handlers
  • Making the chain too complex
  • Not properly documenting the chain
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • Event handling
  • Request processing
  • Command processing
  • Logging
  • Access control
Note: The Chain of Responsibility pattern is particularly useful when you want to give more than one object a chance to handle a request.

4.2 Command Pattern

Pro Tip: Use the Command pattern when you want to encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

The Command pattern encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

When to Use Command Pattern

  • When you want to encapsulate a request as an object
  • When you want to parameterize clients with different requests
  • When you want to queue requests
  • When you want to log requests
  • When you want to support undoable operations

Implementation in Java

Here's how to implement the Command pattern in Java:


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

// Concrete command 1
class ConcreteCommand1 implements Command {
    @Override
    public void execute() {
        System.out.println("Executing ConcreteCommand1");
    }
}

// Concrete command 2
class ConcreteCommand2 implements Command {
    @Override
    public void execute() {
        System.out.println("Executing ConcreteCommand2");
    }
}

// Invoker
class Invoker {
    private Command command;
    
    public void setCommand(Command command) {
        this.command = command;
    }
    
    public void executeCommand() {
        command.execute();
    }
}

Usage Example


public class CommandDemo {
    public static void main(String[] args) {
        Command command1 = new ConcreteCommand1();
        Command command2 = new ConcreteCommand2();
        
        Invoker invoker = new Invoker();
        
        invoker.setCommand(command1);
        invoker.executeCommand();
        
        invoker.setCommand(command2);
        invoker.executeCommand();
    }
}

Best Practices

  • Use interfaces for the command
  • Keep the command simple and focused
  • Consider using dependency injection
  • Document the command
  • Use meaningful names for commands

Common Pitfalls

  • Creating too many commands
  • Making the command too complex
  • Not properly documenting the command
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • Event handling
  • Request processing
  • Command processing
  • Logging
  • Access control
Note: The Command pattern is particularly useful when you want to encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

4.3 Interpreter Pattern

Pro Tip: Use the Interpreter pattern when you want to define a grammar for a language and interpret sentences in the language.

The Interpreter pattern defines a grammar for a language and interprets sentences in the language.

When to Use Interpreter Pattern

  • When you want to define a grammar for a language and interpret sentences in the language
  • When you want to implement a simple language interpreter
  • When you want to implement a complex expression parser
  • When you want to implement a rule-based system

Implementation in Java

Here's how to implement the Interpreter pattern in Java:


// Expression interface
interface Expression {
    boolean interpret(String context);
}

// Concrete expression 1
class TerminalExpression implements Expression {
    private String data;
    
    public TerminalExpression(String data) {
        this.data = data;
    }
    
    @Override
    public boolean interpret(String context) {
        return context.contains(data);
    }
}

// Concrete expression 2
class OrExpression implements Expression {
    private Expression expr1;
    private Expression expr2;
    
    public OrExpression(Expression expr1, Expression expr2) {
        this.expr1 = expr1;
        this.expr2 = expr2;
    }
    
    @Override
    public boolean interpret(String context) {
        return expr1.interpret(context) || expr2.interpret(context);
    }
}

// Concrete expression 3
class AndExpression implements Expression {
    private Expression expr1;
    private Expression expr2;
    
    public AndExpression(Expression expr1, Expression expr2) {
        this.expr1 = expr1;
        this.expr2 = expr2;
    }
    
    @Override
    public boolean interpret(String context) {
        return expr1.interpret(context) && expr2.interpret(context);
    }
}

// Context
class Context {
    private String input;
    private Expression expression;
    
    public Context(String input) {
        this.input = input;
    }
    
    public void setExpression(Expression expression) {
        this.expression = expression;
    }
    
    public boolean evaluate() {
        return expression.interpret(input);
    }
}

Usage Example


public class InterpreterDemo {
    public static void main(String[] args) {
        Expression isMale = getMaleExpression();
        Expression isMarriedWoman = getMarriedWomanExpression();
        
        System.out.println("John is male? " + isMale.interpret("John"));
        System.out.println("Julie is a married woman? " + isMarriedWoman.interpret("Married Julie"));
    }
    
    private static Expression getMaleExpression() {
        Expression robert = new TerminalExpression("Robert");
        Expression john = new TerminalExpression("John");
        return new OrExpression(robert, john);
    }
    
    private static Expression getMarriedWomanExpression() {
        Expression julie = new TerminalExpression("Julie");
        Expression married = new TerminalExpression("Married");
        return new AndExpression(julie, married);
    }
}

Best Practices

  • Use interfaces for the expression
  • Keep the expression simple and focused
  • Consider using dependency injection
  • Document the expression
  • Use meaningful names for expressions

Common Pitfalls

  • Creating too many expressions
  • Making the expression too complex
  • Not properly documenting the expression
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • Rule-based systems
  • Expression parsers
  • Query languages
  • Data validation
  • Natural language processing
Note: The Interpreter pattern is particularly useful when you want to define a grammar for a language and interpret sentences in the language.

4.4 Iterator Pattern

Pro Tip: Use the Iterator pattern when you want to access elements of an aggregate object sequentially without exposing its underlying representation.

The Iterator pattern provides a way to access elements of an aggregate object sequentially without exposing its underlying representation.

When to Use Iterator Pattern

  • When you want to access elements of an aggregate object sequentially
  • When you want to hide the underlying representation of an aggregate
  • When you want to provide a uniform interface for accessing different aggregates
  • When you want to support multiple traversals of an aggregate

Implementation in Java

Here's how to implement the Iterator pattern in Java:


// Aggregate interface
interface Container {
    Iterator getIterator();
}

// Concrete aggregate
class NameRepository implements Container {
    private String[] names = {"Robert", "John", "Julie", "Lora"};
    
    @Override
    public Iterator getIterator() {
        return new NameIterator();
    }
    
    private class NameIterator implements Iterator {
        int index;
        
        @Override
        public boolean hasNext() {
            return index < names.length;
        }
        
        @Override
        public Object next() {
            if (this.hasNext()) {
                return names[index++];
            }
            return null;
        }
    }
}

Usage Example


public class IteratorDemo {
    public static void main(String[] args) {
        NameRepository nameRepository = new NameRepository();
        
        for (Iterator iter = nameRepository.getIterator(); iter.hasNext();) {
            String name = (String) iter.next();
            System.out.println("Name: " + name);
        }
    }
}

Best Practices

  • Use interfaces for the aggregate and iterator
  • Keep the iterator simple and focused
  • Consider using dependency injection
  • Document the iterator
  • Use meaningful names for iterators

Common Pitfalls

  • Creating too many iterators
  • Making the iterator too complex
  • Not properly documenting the iterator
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • GUI frameworks
  • Database access layers
  • Component architectures
  • Tree structures
  • Data processing
Note: The Iterator pattern is particularly useful when you want to access elements of an aggregate object sequentially without exposing its underlying representation.

4.5 Mediator Pattern

Pro Tip: Use the Mediator pattern when you want to reduce communication complexity between multiple objects.

The Mediator pattern reduces communication complexity between multiple objects.

When to Use Mediator Pattern

  • When you want to reduce communication complexity between multiple objects
  • When you want to centralize communication between objects
  • When you want to implement a publish-subscribe system
  • When you want to implement a chat system

Implementation in Java

Here's how to implement the Mediator pattern in Java:


// Mediator interface
interface ChatMediator {
    void sendMessage(String message, User user);
}

// Concrete mediator
class ChatRoom implements ChatMediator {
    private List users;
    
    public ChatRoom() {
        this.users = new ArrayList<>();
    }
    
    public void addUser(User user) {
        users.add(user);
    }
    
    @Override
    public void sendMessage(String message, User user) {
        for (User u : users) {
            if (u != user) {
                u.receive(message);
            }
        }
    }
}

// Colleague interface
interface User {
    void send(String message);
    void receive(String message);
}

// Concrete colleague
class UserImpl implements User {
    private String name;
    private ChatMediator chatMediator;
    
    public UserImpl(String name, ChatMediator chatMediator) {
        this.name = name;
        this.chatMediator = chatMediator;
    }
    
    @Override
    public void send(String message) {
        chatMediator.sendMessage(message, this);
    }
    
    @Override
    public void receive(String message) {
        System.out.println(name + " received: " + message);
    }
}

Usage Example


public class MediatorDemo {
    public static void main(String[] args) {
        ChatMediator chatMediator = new ChatRoom();
        
        User user1 = new UserImpl("John", chatMediator);
        User user2 = new UserImpl("Julie", chatMediator);
        User user3 = new UserImpl("Lora", chatMediator);
        
        chatMediator.addUser(user1);
        chatMediator.addUser(user2);
        chatMediator.addUser(user3);
        
        user1.send("Hello everyone!");
    }
}

Best Practices

  • Use interfaces for the mediator and colleagues
  • Keep the mediator simple and focused
  • Consider using dependency injection
  • Document the mediator
  • Use meaningful names for colleagues

Common Pitfalls

  • Creating too many mediators
  • Making the mediator too complex
  • Not properly documenting the mediator
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • Chat systems
  • Event handling
  • Rule-based systems
  • Data processing
  • Query processors
Note: The Mediator pattern is particularly useful when you want to reduce communication complexity between multiple objects.

4.6 Memento Pattern

Pro Tip: Use the Memento pattern when you want to save and restore the internal state of an object.

The Memento pattern saves and restores the internal state of an object.

When to Use Memento Pattern

  • When you want to save and restore the internal state of an object
  • When you want to implement undo/redo functionality
  • When you want to implement a snapshot mechanism
  • When you want to implement a backup mechanism

Implementation in Java

Here's how to implement the Memento pattern in Java:


// Originator class
class Originator {
    private String state;
    
    public void setState(String state) {
        this.state = state;
    }
    
    public String getState() {
        return state;
    }
    
    public Memento saveStateToMemento() {
        return new Memento(state);
    }
    
    public void getStateFromMemento(Memento memento) {
        state = memento.getState();
    }
}

// Memento class
class Memento {
    private String state;
    
    public Memento(String state) {
        this.state = state;
    }
    
    public String getState() {
        return state;
    }
}

// Caretaker class
class Caretaker {
    private List mementoList = new ArrayList<>();
    
    public void add(Memento state) {
        mementoList.add(state);
    }
    
    public Memento get(int index) {
        return mementoList.get(index);
    }
}

Usage Example


public class MementoDemo {
    public static void main(String[] args) {
        Originator originator = new Originator();
        Caretaker caretaker = new Caretaker();
        
        originator.setState("State #1");
        originator.setState("State #2");
        caretaker.add(originator.saveStateToMemento());
        
        originator.setState("State #3");
        originator.setState("State #4");
        caretaker.add(originator.saveStateToMemento());
        
        originator.getStateFromMemento(caretaker.get(0));
        System.out.println("First saved state: " + originator.getState());
        
        originator.getStateFromMemento(caretaker.get(1));
        System.out.println("Second saved state: " + originator.getState());
    }
}

Best Practices

  • Use interfaces for the originator and memento
  • Keep the memento simple and focused
  • Consider using dependency injection
  • Document the memento
  • Use meaningful names for mementos

Common Pitfalls

  • Creating too many mementos
  • Making the memento too complex
  • Not properly documenting the memento
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • Undo/redo functionality
  • Snapshot mechanism
  • Backup mechanism
  • State management
  • Data processing
Note: The Memento pattern is particularly useful when you want to save and restore the internal state of an object.

4.7 Observer Pattern

Pro Tip: Use the Observer pattern when you want to define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

When to Use Observer Pattern

  • When you want to define a one-to-many dependency between objects
  • When you want to notify multiple objects about changes
  • When you want to implement a publish-subscribe system
  • When you want to implement a chat system

Implementation in Java

Here's how to implement the Observer pattern in Java:


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

// Concrete subject
class WeatherData implements Subject {
    private List observers;
    private float temperature;
    private float humidity;
    private float pressure;
    
    public WeatherData() {
        observers = new ArrayList<>();
    }
    
    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }
    
    @Override
    public void removeObserver(Observer o) {
        observers.remove(o);
    }
    
    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(temperature, humidity, pressure);
        }
    }
    
    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
    
    private void measurementsChanged() {
        notifyObservers();
    }
}

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

// Concrete observer
class CurrentConditionsDisplay implements Observer {
    private float temperature;
    private float humidity;
    
    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }
    
    private void display() {
        System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
    }
}

Usage Example


public class ObserverDemo {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        
        Observer currentConditionsDisplay = new CurrentConditionsDisplay();
        weatherData.registerObserver(currentConditionsDisplay);
        
        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(82, 70, 29.2f);
        weatherData.setMeasurements(78, 90, 29.2f);
    }
}

Best Practices

  • Use interfaces for the subject and observers
  • Keep the subject simple and focused
  • Consider using dependency injection
  • Document the subject
  • Use meaningful names for observers

Common Pitfalls

  • Creating too many observers
  • Making the subject too complex
  • Not properly documenting the subject
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • Event handling
  • Rule-based systems
  • Data processing
  • Query processors
  • Chat systems
Note: The Observer pattern is particularly useful when you want to define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

4.8 State Pattern

Pro Tip: Use the State pattern when you want to allow an object to change its behavior when its internal state changes.

The State pattern allows an object to change its behavior when its internal state changes.

When to Use State Pattern

  • When you want to allow an object to change its behavior when its internal state changes
  • When you want to implement a finite state machine
  • When you want to implement a state-specific behavior
  • When you want to implement a context-dependent behavior

Implementation in Java

Here's how to implement the State pattern in Java:


// State interface
interface State {
    void doAction(Context context);
}

// Concrete state 1
class StartState implements State {
    @Override
    public void doAction(Context context) {
        System.out.println("Player is in start state");
        context.setState(this);
    }
}

// Concrete state 2
class StopState implements State {
    @Override
    public void doAction(Context context) {
        System.out.println("Player is in stop state");
        context.setState(this);
    }
}

// Context class
class Context {
    private State state;
    
    public Context() {
        state = null;
    }
    
    public void setState(State state) {
        this.state = state;
    }
    
    public State getState() {
        return state;
    }
}

Usage Example


public class StateDemo {
    public static void main(String[] args) {
        Context context = new Context();
        
        StartState startState = new StartState();
        startState.doAction(context);
        
        StopState stopState = new StopState();
        stopState.doAction(context);
    }
}

Best Practices

  • Use interfaces for the state
  • Keep the state simple and focused
  • Consider using dependency injection
  • Document the state
  • Use meaningful names for states

Common Pitfalls

  • Creating too many states
  • Making the state too complex
  • Not properly documenting the state
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • Finite state machines
  • State-specific behaviors
  • Context-dependent behaviors
  • Data processing
  • Query processors
Note: The State pattern is particularly useful when you want to allow an object to change its behavior when its internal state changes.

4.9 Strategy Pattern

Pro Tip: Use the Strategy pattern when you want to define a family of algorithms, encapsulate each one, and make them interchangeable.

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.

When to Use Strategy Pattern

  • When you want to define a family of algorithms
  • When you want to encapsulate each algorithm
  • When you want to make algorithms interchangeable
  • When you want to implement a pluggable behavior

Implementation in Java

Here's how to implement the Strategy pattern in Java:


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

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

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

// Context class
class Context {
    private Strategy strategy;
    
    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }
    
    public void executeStrategy() {
        strategy.execute();
    }
}

Usage Example


public class StrategyDemo {
    public static void main(String[] args) {
        Context context = new Context();
        
        Strategy strategy1 = new ConcreteStrategy1();
        Strategy strategy2 = new ConcreteStrategy2();
        
        context.setStrategy(strategy1);
        context.executeStrategy();
        
        context.setStrategy(strategy2);
        context.executeStrategy();
    }
}

Best Practices

  • Use interfaces for the strategy
  • Keep the strategy simple and focused
  • Consider using dependency injection
  • Document the strategy
  • Use meaningful names for strategies

Common Pitfalls

  • Creating too many strategies
  • Making the strategy too complex
  • Not properly documenting the strategy
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • Algorithm implementation
  • Data processing
  • Query processors
  • GUI frameworks
  • Chat systems
Note: The Strategy pattern is particularly useful when you want to define a family of algorithms, encapsulate each one, and make them interchangeable.

4.10 Template Method Pattern

Pro Tip: Use the Template Method pattern when you want to let subclasses implement different steps of an algorithm without changing the algorithm's structure.

The Template Method pattern lets subclasses implement different steps of an algorithm without changing the algorithm's structure.

When to Use Template Method Pattern

  • When you want to let subclasses implement different steps of an algorithm without changing the algorithm's structure
  • When you want to implement the invariant parts of an algorithm
  • When you want to implement a template
  • When you want to implement a callback

Implementation in Java

Here's how to implement the Template Method pattern in Java:


// Abstract class
abstract class Game {
    abstract void initialize();
    abstract void startPlay();
    abstract void endPlay();
    
    // Template method
    public final void play() {
        initialize();
        startPlay();
        endPlay();
    }
}

// Concrete class
class Cricket extends Game {
    @Override
    void initialize() {
        System.out.println("Cricket Game Initialized! Start playing.");
    }
    
    @Override
    void startPlay() {
        System.out.println("Cricket Game Started. Enjoy the game!");
    }
    
    @Override
    void endPlay() {
        System.out.println("Cricket Game Finished!");
    }
}

class Football extends Game {
    @Override
    void initialize() {
        System.out.println("Football Game Initialized! Start playing.");
    }
    
    @Override
    void startPlay() {
        System.out.println("Football Game Started. Enjoy the game!");
    }
    
    @Override
    void endPlay() {
        System.out.println("Football Game Finished!");
    }
}

Usage Example


public class TemplateMethodDemo {
    public static void main(String[] args) {
        Game game = new Cricket();
        game.play();
        
        game = new Football();
        game.play();
    }
}

Best Practices

  • Use abstract classes for the template
  • Keep the template simple and focused
  • Consider using dependency injection
  • Document the template
  • Use meaningful names for template methods

Common Pitfalls

  • Creating too many templates
  • Making the template too complex
  • Not properly documenting the template
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • Algorithm implementation
  • Data processing
  • Query processors
  • GUI frameworks
  • Chat systems
Note: The Template Method pattern is particularly useful when you want to let subclasses implement different steps of an algorithm without changing the algorithm's structure.

4.11 Visitor Pattern

Pro Tip: Use the Visitor pattern when you want to define a new operation to a class without changing the class.

The Visitor pattern defines a new operation to a class without changing the class.

When to Use Visitor Pattern

  • When you want to define a new operation to a class without changing the class
  • When you want to avoid code duplication
  • When you want to control the algorithm
  • When you want to implement a template

Implementation in Java

Here's how to implement the Visitor pattern in Java:


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

// Concrete element 1
class ConcreteElement1 implements Element {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

// Concrete element 2
class ConcreteElement2 implements Element {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

// Visitor interface
interface Visitor {
    void visit(ConcreteElement1 element);
    void visit(ConcreteElement2 element);
}

// Concrete visitor
class ConcreteVisitor implements Visitor {
    @Override
    public void visit(ConcreteElement1 element) {
        System.out.println("ConcreteVisitor visited ConcreteElement1");
    }
    
    @Override
    public void visit(ConcreteElement2 element) {
        System.out.println("ConcreteVisitor visited ConcreteElement2");
    }
}

Usage Example


public class VisitorDemo {
    public static void main(String[] args) {
        Element element1 = new ConcreteElement1();
        Element element2 = new ConcreteElement2();
        
        Visitor visitor = new ConcreteVisitor();
        
        element1.accept(visitor);
        element2.accept(visitor);
    }
}

Best Practices

  • Use interfaces for the element
  • Keep the visitor simple and focused
  • Consider using dependency injection
  • Document the visitor
  • Use meaningful names for visitors

Common Pitfalls

  • Creating too many visitors
  • Making the visitor too complex
  • Not properly documenting the visitor
  • Ignoring the single responsibility principle
  • Not considering thread safety

Real-World Applications

  • Data processing
  • Query processors
  • GUI frameworks
  • Chat systems
  • Algorithm implementation
Note: The Visitor pattern is particularly useful when you want to define a new operation to a class without changing the class.

Best Practices and Implementation

Note: Best practices for using design patterns include understanding their purpose, choosing the right pattern for the problem, and applying them correctly.

Best practices for using design patterns include understanding their purpose, choosing the right pattern for the problem, and applying them correctly.

Choosing the Right Pattern

Choosing the right design pattern is crucial for effective software design. Consider the following factors:

  • The problem you're trying to solve
  • The context in which the pattern will be used
  • The trade-offs associated with each pattern

Applying Patterns Correctly

Applying design patterns correctly involves understanding their intent and ensuring they're used in the right context. Here are some tips:

  • Use patterns sparingly
  • Avoid overusing patterns
  • Understand the trade-offs
  • Document your design decisions

Understanding Patterns

Understanding design patterns involves grasping their intent, benefits, and potential drawbacks. Here are some tips:

  • Read about patterns
  • Study examples
  • Practice applying patterns

Documenting Design Decisions

Documenting design decisions is crucial for maintaining and evolving software. Here are some tips:

  • Use a design documentation tool
  • Write clear and concise descriptions
  • Link decisions to specific patterns

Conclusion

Note: Design patterns are powerful tools for software design, but they should be used judiciously.

Design patterns are powerful tools for software design, but they should be used judiciously. They provide proven solutions to common design problems, but they should not be overused or misused.

Summary

This comprehensive guide has covered all 23 Gang of Four (GoF) design patterns in Java. Understanding these patterns and their purpose is crucial for effective software design.

Further Reading

For further reading, consider the following resources:

  • Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides
  • Head First Design Patterns by Eric Freeman and Elisabeth Robson
  • Java Design Patterns by Richard Helm