Skip to content

Structural Design Patterns

Overview

Structural design patterns are concerned with how classes and objects are composed to form larger structures. They help ensure that when parts of a system change, the entire system doesn't need to change. This guide covers essential structural design patterns in Java, their implementation, use cases, advantages, and potential drawbacks.

Prerequisites

  • Solid understanding of Java programming
  • Familiarity with object-oriented programming concepts
  • Basic knowledge of SOLID principles
  • Understanding of class inheritance and interfaces

Learning Objectives

  • Understand the purpose and benefits of structural design patterns
  • Learn when and how to implement different structural patterns
  • Recognize appropriate use cases for each pattern
  • Implement structural patterns in Java applications
  • Understand the trade-offs between different structural patterns
  • Apply best practices when implementing structural patterns

Table of Contents

  1. Introduction to Structural Patterns
  2. Adapter Pattern
  3. Bridge Pattern
  4. Composite Pattern
  5. Decorator Pattern
  6. Facade Pattern
  7. Flyweight Pattern
  8. Proxy Pattern
  9. Best Practices
  10. Common Pitfalls
  11. Comparing Structural Patterns

Introduction to Structural Patterns

Structural design patterns deal with object composition, creating relationships between objects to form larger structures. They help to ensure that if one part of a system changes, the entire system doesn't need to change along with it.

Why Use Structural Patterns?

  1. Flexibility: They provide flexibility in how objects and classes are composed together.
  2. Decoupling: They promote loose coupling between different components.
  3. Simplification: They simplify complex structures by providing abstractions.
  4. Adaptability: They make it easier to adapt objects to work with different interfaces.

When to Use Structural Patterns

  • When you need to organize objects into larger structures
  • When you need to make incompatible interfaces work together
  • When you want to add responsibilities to objects without changing their code
  • When you need to simplify complex subsystems
  • When you want to optimize resource usage with many similar objects

Adapter Pattern

The Adapter pattern converts the interface of a class into another interface clients expect, allowing classes to work together that couldn't otherwise due to incompatible interfaces.

Intent

  • Convert the interface of a class into another interface clients expect
  • Allow classes to work together that couldn't otherwise due to incompatible interfaces
  • Make existing classes work with others without modifying their source code

Implementation

Object Adapter (Composition)

// Target interface that the client expects to use
interface MediaPlayer {
    void play(String audioType, String fileName);
}

// Adaptee interface - the interface that needs adapting
interface AdvancedMediaPlayer {
    void playVlc(String fileName);
    void playMp4(String fileName);
}

// Concrete Adaptee implementations
class VlcPlayer implements AdvancedMediaPlayer {
    @Override
    public void playVlc(String fileName) {
        System.out.println("Playing vlc file: " + fileName);
    }

    @Override
    public void playMp4(String fileName) {
        // Do nothing
    }
}

class Mp4Player implements AdvancedMediaPlayer {
    @Override
    public void playVlc(String fileName) {
        // Do nothing
    }

    @Override
    public void playMp4(String fileName) {
        System.out.println("Playing mp4 file: " + fileName);
    }
}

// Adapter using composition
class MediaAdapter implements MediaPlayer {
    private AdvancedMediaPlayer advancedMediaPlayer;

    public MediaAdapter(String audioType) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMediaPlayer = new VlcPlayer();
        } else if (audioType.equalsIgnoreCase("mp4")) {
            advancedMediaPlayer = new Mp4Player();
        }
    }

    @Override
    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMediaPlayer.playVlc(fileName);
        } else if (audioType.equalsIgnoreCase("mp4")) {
            advancedMediaPlayer.playMp4(fileName);
        }
    }
}

// Client
class AudioPlayer implements MediaPlayer {
    private MediaAdapter mediaAdapter;

    @Override
    public void play(String audioType, String fileName) {
        // Built-in support for mp3 files
        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. " + audioType + " format not supported");
        }
    }
}

// Usage
MediaPlayer player = new AudioPlayer();
player.play("mp3", "song.mp3");    // Playing mp3 file: song.mp3
player.play("vlc", "movie.vlc");   // Playing vlc file: movie.vlc
player.play("mp4", "video.mp4");   // Playing mp4 file: video.mp4
player.play("avi", "video.avi");   // Invalid media. avi format not supported

Class Adapter (Inheritance)

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

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

// Class Adapter using inheritance
class ClassAdapter extends Adaptee implements Target {
    @Override
    public void request() {
        specificRequest();
    }
}

// Usage
Target target = new ClassAdapter();
target.request(); // Outputs: Specific request from Adaptee

Real-World Example: Legacy System Integration

// Legacy database system
class LegacyDatabase {
    public void connect() {
        System.out.println("Connected to legacy database");
    }

    public void executeQuery(String rawSql) {
        System.out.println("Executing raw SQL: " + rawSql);
    }

    public void disconnect() {
        System.out.println("Disconnected from legacy database");
    }
}

// Modern database interface expected by the application
interface ModernDatabase {
    void openConnection();
    void queryData(String tableName, String[] columns, String condition);
    void closeConnection();
}

// Adapter for the legacy database system
class DatabaseAdapter implements ModernDatabase {
    private LegacyDatabase legacyDb = new LegacyDatabase();

    @Override
    public void openConnection() {
        legacyDb.connect();
    }

    @Override
    public void queryData(String tableName, String[] columns, String condition) {
        StringBuilder sql = new StringBuilder("SELECT ");

        if (columns.length > 0) {
            for (int i = 0; i < columns.length; i++) {
                sql.append(columns[i]);
                if (i < columns.length - 1) {
                    sql.append(", ");
                }
            }
        } else {
            sql.append("*");
        }

        sql.append(" FROM ").append(tableName);

        if (condition != null && !condition.isEmpty()) {
            sql.append(" WHERE ").append(condition);
        }

        legacyDb.executeQuery(sql.toString());
    }

    @Override
    public void closeConnection() {
        legacyDb.disconnect();
    }
}

// Usage
ModernDatabase db = new DatabaseAdapter();
db.openConnection();
db.queryData("users", new String[]{"id", "name", "email"}, "active = true");
db.closeConnection();

Two-Way Adapter

// Interface A
interface SquarePeg {
    void insert();
}

// Interface B
interface RoundPeg {
    void insertIntoHole();
}

// Two-way adapter implementing both interfaces
class PegAdapter implements SquarePeg, RoundPeg {
    @Override
    public void insert() {
        System.out.println("Square peg inserted");
        // Can call insertIntoHole() if needed
    }

    @Override
    public void insertIntoHole() {
        System.out.println("Round peg inserted into hole");
        // Can call insert() if needed
    }
}

// Usage
SquarePeg squarePeg = new PegAdapter();
squarePeg.insert();

RoundPeg roundPeg = (RoundPeg) squarePeg; // Same object, different interface
roundPeg.insertIntoHole();

When to Use the Adapter Pattern

  • When you want to use an existing class, but its interface does not match the one you need
  • When you want to create a reusable class that cooperates with classes that don't necessarily have compatible interfaces
  • When you need to use several existing subclasses but don't want to adapt their interfaces by subclassing each one
  • When you want to make your code work with third-party libraries
  • When you want to integrate legacy code with modern code without changing the legacy implementation

Advantages

  • Allows incompatible interfaces to work together
  • Promotes reusability of existing code
  • Improves maintainability by separating client code from adapted code
  • Enables interoperability between different systems
  • Single Responsibility Principle: separates interface conversion from the business logic

Disadvantages

  • Increases complexity by adding an extra layer
  • Sometimes overused when a simple refactoring would be better
  • In class adapters, can only adapt to one class (due to Java's single inheritance)
  • Debugging can be harder due to the extra indirection

Bridge Pattern

The Bridge pattern decouples an abstraction from its implementation so that the two can vary independently. It involves an interface acting as a bridge between the abstract class and implementation classes.

Intent

  • Decouple an abstraction from its implementation so that the two can vary independently
  • Avoid permanent binding between an abstraction and its implementation
  • Allow both the abstraction and its implementation to be extended through inheritance
  • Hide implementation details from clients

Implementation

// Implementor interface
interface DrawAPI {
    void drawCircle(double x, double y, double radius);
}

// Concrete Implementors
class RedCircle implements DrawAPI {
    @Override
    public void drawCircle(double x, double y, double radius) {
        System.out.printf("Drawing Circle[ color: red, center: (%f, %f), radius: %f ]%n", x, y, radius);
    }
}

class GreenCircle implements DrawAPI {
    @Override
    public void drawCircle(double x, double y, double radius) {
        System.out.printf("Drawing Circle[ color: green, center: (%f, %f), radius: %f ]%n", x, y, radius);
    }
}

// Abstraction
abstract class Shape {
    protected DrawAPI drawAPI;

    protected Shape(DrawAPI drawAPI) {
        this.drawAPI = drawAPI;
    }

    public abstract void draw();
}

// Refined Abstraction
class Circle extends Shape {
    private double x, y, radius;

    public Circle(double x, double y, double radius, DrawAPI drawAPI) {
        super(drawAPI);
        this.x = x;
        this.y = y;
        this.radius = radius;
    }

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

// Usage
DrawAPI redCircle = new RedCircle();
DrawAPI greenCircle = new GreenCircle();

Shape redCircleShape = new Circle(100, 100, 10, redCircle);
Shape greenCircleShape = new Circle(200, 200, 15, greenCircle);

redCircleShape.draw();   // Drawing Circle[ color: red, center: (100.000000, 100.000000), radius: 10.000000 ]
greenCircleShape.draw(); // Drawing Circle[ color: green, center: (200.000000, 200.000000), radius: 15.000000 ]

Extended Example: Messaging System

// Implementor interface
interface MessageSender {
    void sendMessage(String message, String recipient);
}

// Concrete Implementors
class EmailSender implements MessageSender {
    @Override
    public void sendMessage(String message, String recipient) {
        System.out.println("Sending Email to " + recipient + ": " + message);
    }
}

class SMSSender implements MessageSender {
    @Override
    public void sendMessage(String message, String recipient) {
        System.out.println("Sending SMS to " + recipient + ": " + message);
    }
}

class WhatsAppSender implements MessageSender {
    @Override
    public void sendMessage(String message, String recipient) {
        System.out.println("Sending WhatsApp message to " + recipient + ": " + message);
    }
}

// Abstraction
abstract class Message {
    protected MessageSender messageSender;

    protected Message(MessageSender messageSender) {
        this.messageSender = messageSender;
    }

    public abstract void send();
}

// Refined Abstractions
class TextMessage extends Message {
    private String text;
    private String recipient;

    public TextMessage(String text, String recipient, MessageSender messageSender) {
        super(messageSender);
        this.text = text;
        this.recipient = recipient;
    }

    @Override
    public void send() {
        messageSender.sendMessage(text, recipient);
    }
}

class UrgentMessage extends Message {
    private String text;
    private String recipient;

    public UrgentMessage(String text, String recipient, MessageSender messageSender) {
        super(messageSender);
        this.text = text;
        this.recipient = recipient;
    }

    @Override
    public void send() {
        messageSender.sendMessage("URGENT: " + text, recipient);
    }
}

// Usage
MessageSender emailSender = new EmailSender();
MessageSender smsSender = new SMSSender();
MessageSender whatsAppSender = new WhatsAppSender();

Message textEmailMessage = new TextMessage("Hello, how are you?", "john@example.com", emailSender);
Message urgentSmsMessage = new UrgentMessage("Meeting in 10 minutes!", "123-456-7890", smsSender);
Message urgentWhatsAppMessage = new UrgentMessage("Call me back ASAP", "+1-234-567-8901", whatsAppSender);

textEmailMessage.send();      // Sending Email to john@example.com: Hello, how are you?
urgentSmsMessage.send();      // Sending SMS to 123-456-7890: URGENT: Meeting in 10 minutes!
urgentWhatsAppMessage.send(); // Sending WhatsApp message to +1-234-567-8901: URGENT: Call me back ASAP

When to Use the Bridge Pattern

  • When you want to avoid a permanent binding between an abstraction and its implementation
  • When both the abstraction and its implementation should be extensible through subclasses
  • When changes in the implementation should not impact the client code
  • When you have a proliferation of classes resulting from a coupled interface and numerous implementations
  • When you want to share an implementation among multiple objects
  • When you want to hide implementation details completely from clients

Advantages

  • Decouples interface from implementation
  • Improves extensibility (you can extend the abstraction and implementation hierarchies independently)
  • Hides implementation details from clients
  • Allows for dynamic switching of implementations at runtime
  • Follows Open/Closed Principle by allowing new abstractions and implementations to be added separately

Disadvantages

  • Increases complexity due to additional indirection
  • Can be overkill for simple applications
  • Requires designing the proper abstractions up front

Composite Pattern

The Composite pattern composes objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly.

Intent

  • Compose objects into tree structures to represent part-whole hierarchies
  • Allow clients to treat individual objects and compositions of objects uniformly
  • Make it easy to add new kinds of components
  • Create recursive tree structures with composite and leaf nodes

Implementation

// Component interface
interface Component {
    void operation();
    void add(Component component);
    void remove(Component component);
    Component getChild(int index);
}

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

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

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

    @Override
    public void add(Component component) {
        // Leaf nodes cannot have children
        throw new UnsupportedOperationException("Cannot add to a leaf");
    }

    @Override
    public void remove(Component component) {
        // Leaf nodes cannot have children
        throw new UnsupportedOperationException("Cannot remove from a leaf");
    }

    @Override
    public Component getChild(int index) {
        // Leaf nodes cannot have children
        throw new UnsupportedOperationException("Cannot get child from a leaf");
    }
}

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

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

    @Override
    public void operation() {
        System.out.println("Composite " + name + ": operation");
        // Operation on all children
        for (Component component : children) {
            component.operation();
        }
    }

    @Override
    public void add(Component component) {
        children.add(component);
    }

    @Override
    public void remove(Component component) {
        children.remove(component);
    }

    @Override
    public Component getChild(int index) {
        return children.get(index);
    }
}

// Usage
Component leaf1 = new Leaf("A");
Component leaf2 = new Leaf("B");
Component leaf3 = new Leaf("C");

Component composite1 = new Composite("X");
composite1.add(leaf1);
composite1.add(leaf2);

Component composite2 = new Composite("Y");
composite2.add(leaf3);
composite2.add(composite1);

// Treat composite and leaf uniformly
composite2.operation();

File System Example

// Component
abstract class FileSystemComponent {
    protected String name;
    protected int size;

    public FileSystemComponent(String name, int size) {
        this.name = name;
        this.size = size;
    }

    public abstract int getSize();
    public abstract void printStructure(String indent);

    public String getName() {
        return name;
    }
}

// Leaf
class File extends FileSystemComponent {
    public File(String name, int size) {
        super(name, size);
    }

    @Override
    public int getSize() {
        return size;
    }

    @Override
    public void printStructure(String indent) {
        System.out.println(indent + "File: " + name + " (" + size + " KB)");
    }
}

// Composite
class Directory extends FileSystemComponent {
    private List<FileSystemComponent> children = new ArrayList<>();

    public Directory(String name) {
        super(name, 0);
    }

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

    public void removeComponent(FileSystemComponent component) {
        children.remove(component);
    }

    @Override
    public int getSize() {
        int totalSize = 0;
        for (FileSystemComponent component : children) {
            totalSize += component.getSize();
        }
        return totalSize;
    }

    @Override
    public void printStructure(String indent) {
        System.out.println(indent + "Directory: " + name + " (" + getSize() + " KB)");
        for (FileSystemComponent component : children) {
            component.printStructure(indent + "  ");
        }
    }
}

// Usage
File file1 = new File("file1.txt", 5);
File file2 = new File("file2.txt", 10);
File file3 = new File("file3.txt", 7);
File file4 = new File("file4.txt", 20);

Directory dir1 = new Directory("docs");
dir1.addComponent(file1);
dir1.addComponent(file2);

Directory dir2 = new Directory("images");
dir2.addComponent(file3);

Directory root = new Directory("root");
root.addComponent(dir1);
root.addComponent(dir2);
root.addComponent(file4);

// Print structure and size
root.printStructure("");
System.out.println("Total size: " + root.getSize() + " KB");

Transparency vs. Safety

There are two approaches to implementing the Composite pattern:

Transparent Approach (as shown above)

  • Component declares all operations (both leaf and composite)
  • Client can treat all objects uniformly
  • Type safety is sacrificed (leaf nodes throw exceptions for composite operations)

Safe Approach

// Component interface with only common operations
interface Component {
    void operation();
}

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

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

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

// Composite class with additional methods
class Composite implements Component {
    private List<Component> children = new ArrayList<>();
    private String name;

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

    @Override
    public void operation() {
        System.out.println("Composite " + name + ": operation");
        for (Component component : children) {
            component.operation();
        }
    }

    // Composite-specific methods
    public void add(Component component) {
        children.add(component);
    }

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

    public Component getChild(int index) {
        return children.get(index);
    }
}

// Usage requires type checking for Composite operations
Component component = getComponent(); // Some method that returns a Component
if (component instanceof Composite) {
    Composite composite = (Composite) component;
    composite.add(new Leaf("New Leaf"));
}

When to Use the Composite Pattern

  • When you want to represent part-whole hierarchies of objects
  • When you want clients to be able to ignore the difference between compositions of objects and individual objects
  • When the structure can have any level of complexity
  • When you want the client code to work with all objects in the hierarchy uniformly
  • When you're dealing with tree-structured data

Advantages

  • Defines class hierarchies containing primitive and complex objects
  • Makes it easier to add new types of components
  • Provides flexibility of structure with manageable components
  • Simplifies client code by allowing it to treat complex and individual objects uniformly
  • Follows the Open/Closed Principle by allowing new components to be added without changing existing code

Disadvantages

  • Can make the design overly general, making it harder to restrict certain components
  • Can be difficult to provide a common interface for classes whose functionality differs widely
  • In the transparent approach, type safety is sacrificed
  • Difficult to restrict components of a particular composite to only particular types

Decorator Pattern

The Decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

Intent

  • Add responsibilities to individual objects dynamically and transparently
  • Provide an alternative to subclassing for extending functionality
  • Support the Open/Closed Principle by allowing functionality to be added without modifying existing code
  • Allow responsibilities to be added and removed at runtime

Implementation

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

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

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

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

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

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

    @Override
    public void operation() {
        super.operation();
        addedBehavior();
    }

    private void addedBehavior() {
        System.out.println("ConcreteDecoratorA: addedBehavior");
    }
}

class ConcreteDecoratorB extends Decorator {
    public ConcreteDecoratorB(Component component) {
        super(component);
    }

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

// Usage
Component component = new ConcreteComponent();
Component decoratedA = new ConcreteDecoratorA(component);
Component decoratedB = new ConcreteDecoratorB(decoratedA);

// Call operation on the chain of decorators
decoratedB.operation();

Coffee Shop Example

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

// Concrete Component
class SimpleCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "Simple Coffee";
    }

    @Override
    public double getCost() {
        return 2.0;
    }
}

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

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

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

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

// Concrete Decorators
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription() + ", with milk";
    }

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

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription() + ", with sugar";
    }

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

class WhippedCreamDecorator extends CoffeeDecorator {
    public WhippedCreamDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription() + ", with whipped cream";
    }

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

// Usage
Coffee simpleCoffee = new SimpleCoffee();
System.out.println(simpleCoffee.getDescription() + ": $" + simpleCoffee.getCost());

Coffee milkCoffee = new MilkDecorator(simpleCoffee);
System.out.println(milkCoffee.getDescription() + ": $" + milkCoffee.getCost());

Coffee sweetMilkCoffee = new SugarDecorator(milkCoffee);
System.out.println(sweetMilkCoffee.getDescription() + ": $" + sweetMilkCoffee.getCost());

Coffee whippedSweetMilkCoffee = new WhippedCreamDecorator(sweetMilkCoffee);
System.out.println(whippedSweetMilkCoffee.getDescription() + ": $" + whippedSweetMilkCoffee.getCost());

Java I/O Decorators

Java's I/O classes are a real-world example of the Decorator pattern:

// Using Java's built-in I/O decorators
import java.io.*;

public class JavaIODecoratorExample {
    public static void main(String[] args) throws IOException {
        // Creating a chain of decorators for reading data
        try (InputStream fileInputStream = new FileInputStream("file.txt");
             InputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
             InputStream dataInputStream = new DataInputStream(bufferedInputStream)) {

            // Now we can use methods from DataInputStream
            // while benefiting from buffering and file input
            byte data = dataInputStream.readByte();
        }

        // Creating a chain of decorators for writing data
        try (OutputStream fileOutputStream = new FileOutputStream("output.txt");
             OutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
             PrintStream printStream = new PrintStream(bufferedOutputStream)) {

            // Now we can use methods from PrintStream
            // while benefiting from buffering and file output
            printStream.println("Hello, Decorator Pattern!");
        }
    }
}

When to Use the Decorator Pattern

  • When you need to add responsibilities to objects dynamically and transparently
  • When extension by subclassing is impractical (would lead to an explosion of subclasses)
  • When you want to add functionality to individual objects without affecting others
  • When you want to add and remove responsibilities at runtime
  • When your design should allow for a potentially unlimited variety of additional functionalities

Advantages

  • More flexible than inheritance for extending functionality
  • Allows responsibilities to be added and removed at runtime
  • Allows combining multiple behaviors by nesting decorators
  • Follows the Single Responsibility Principle by dividing functionality into classes
  • Follows the Open/Closed Principle by allowing extension without modification

Disadvantages

  • Can result in many small objects that look similar
  • Decorators can complicate the process of instantiating the component
  • Difficult to understand and debug code with many layers of decoration
  • Some implementations of the decorator pattern (especially those with extensive state) may not be identical to the component they wrap

Facade Pattern

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.

Intent

  • Provide a unified interface to a set of interfaces in a subsystem
  • Define a higher-level interface that makes the subsystem easier to use
  • Shield clients from complex subsystem components
  • Reduce dependencies on external code
  • Promote loose coupling between subsystems and clients

Implementation

// Complex subsystem classes
class SubsystemA {
    public void operationA() {
        System.out.println("SubsystemA: Operation A");
    }
}

class SubsystemB {
    public void operationB() {
        System.out.println("SubsystemB: Operation B");
    }
}

class SubsystemC {
    public void operationC() {
        System.out.println("SubsystemC: Operation C");
    }
}

// Facade
class Facade {
    private SubsystemA subsystemA;
    private SubsystemB subsystemB;
    private SubsystemC subsystemC;

    public Facade() {
        this.subsystemA = new SubsystemA();
        this.subsystemB = new SubsystemB();
        this.subsystemC = new SubsystemC();
    }

    // Methods that delegate to subsystems
    public void operation1() {
        System.out.println("Facade: Operation 1");
        subsystemA.operationA();
        subsystemB.operationB();
    }

    public void operation2() {
        System.out.println("Facade: Operation 2");
        subsystemB.operationB();
        subsystemC.operationC();
    }
}

// Client code
Facade facade = new Facade();
facade.operation1();
facade.operation2();

Home Theater Example

// Complex subsystem classes
class DVDPlayer {
    public void on() {
        System.out.println("DVD Player is ON");
    }

    public void off() {
        System.out.println("DVD Player is OFF");
    }

    public void play(String movie) {
        System.out.println("DVD Player is playing: " + movie);
    }

    public void stop() {
        System.out.println("DVD Player stopped");
    }
}

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

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

    public void setVolume(int level) {
        System.out.println("Amplifier volume set to: " + level);
    }

    public void setDvd(DVDPlayer dvd) {
        System.out.println("Amplifier connected to DVD player");
    }
}

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

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

    public void wideScreenMode() {
        System.out.println("Projector in widescreen mode (16:9)");
    }
}

class TheaterLights {
    public void dim(int level) {
        System.out.println("Theater lights dimming to " + level + "%");
    }

    public void on() {
        System.out.println("Theater lights ON");
    }
}

class Screen {
    public void down() {
        System.out.println("Theater screen going down");
    }

    public void up() {
        System.out.println("Theater screen going up");
    }
}

// Facade
class HomeTheaterFacade {
    private Amplifier amplifier;
    private DVDPlayer dvdPlayer;
    private Projector projector;
    private TheaterLights lights;
    private Screen screen;

    public HomeTheaterFacade(Amplifier amplifier, DVDPlayer dvdPlayer, 
                           Projector projector, TheaterLights lights, Screen screen) {
        this.amplifier = amplifier;
        this.dvdPlayer = dvdPlayer;
        this.projector = projector;
        this.lights = lights;
        this.screen = screen;
    }

    public void watchMovie(String movie) {
        System.out.println("=== Getting ready to watch a movie ===");
        lights.dim(10);
        screen.down();
        projector.on();
        projector.wideScreenMode();
        amplifier.on();
        amplifier.setDvd(dvdPlayer);
        amplifier.setVolume(5);
        dvdPlayer.on();
        dvdPlayer.play(movie);
    }

    public void endMovie() {
        System.out.println("=== Shutting down movie theater ===");
        dvdPlayer.stop();
        dvdPlayer.off();
        amplifier.off();
        projector.off();
        screen.up();
        lights.on();
    }
}

// Client code
Amplifier amplifier = new Amplifier();
DVDPlayer dvdPlayer = new DVDPlayer();
Projector projector = new Projector();
TheaterLights lights = new TheaterLights();
Screen screen = new Screen();

HomeTheaterFacade homeTheater = new HomeTheaterFacade(
    amplifier, dvdPlayer, projector, lights, screen);

// Using the facade
homeTheater.watchMovie("Inception");
// ... movie is playing ...
homeTheater.endMovie();

Database Facade Example

// Complex database subsystem
class Connection {
    public void open(String connectionString) {
        System.out.println("Opening database connection to: " + connectionString);
    }

    public void close() {
        System.out.println("Closing database connection");
    }
}

class Command {
    private Connection connection;

    public Command(Connection connection) {
        this.connection = connection;
    }

    public void execute(String query) {
        System.out.println("Executing query: " + query);
    }
}

class Transaction {
    public void begin() {
        System.out.println("Beginning transaction");
    }

    public void commit() {
        System.out.println("Committing transaction");
    }

    public void rollback() {
        System.out.println("Rolling back transaction");
    }
}

class ResultSet {
    public void read() {
        System.out.println("Reading data from result set");
    }

    public void close() {
        System.out.println("Closing result set");
    }
}

// Database Facade
class DatabaseFacade {
    private Connection connection;
    private Transaction transaction;

    public DatabaseFacade() {
        connection = new Connection();
        transaction = new Transaction();
    }

    public void executeQuery(String connectionString, String query) {
        connection.open(connectionString);
        Command command = new Command(connection);
        ResultSet resultSet = new ResultSet();

        try {
            transaction.begin();
            command.execute(query);
            resultSet.read();
            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
            System.out.println("Error executing query: " + e.getMessage());
        } finally {
            resultSet.close();
            connection.close();
        }
    }
}

// Client code
DatabaseFacade dbFacade = new DatabaseFacade();
dbFacade.executeQuery("jdbc:mysql://localhost:3306/mydb", "SELECT * FROM users");

When to Use the Facade Pattern

  • When you want to provide a simple interface to a complex subsystem
  • When there are many dependencies between clients and the implementation classes of an abstraction
  • When you want to layer your subsystems
  • When you want to decouple your client code from subsystem components
  • When you need to simplify a large, complex API
  • When you want to create an entry point to each level of a layered software

Advantages

  • Shields clients from subsystem components, reducing coupling
  • Promotes subsystem independence and portability
  • Simplifies the usage of complex systems
  • Provides a context-specific interface to a large, general-purpose API
  • Helps in applying the principle of least knowledge (clients only talk to the facade)
  • Enables organizing systems into layers

Disadvantages

  • A facade can become a god object coupled to all classes of an app
  • The facade might add complexity if the underlying system is simple
  • The facade may hide important configuration options and flexibility
  • Might introduce a performance overhead if not designed carefully

Flyweight Pattern

The Flyweight pattern uses sharing to support large numbers of fine-grained objects efficiently. It reduces memory usage by sharing common state between multiple objects instead of keeping all of the data in each object.

Intent

  • Use sharing to support large numbers of fine-grained objects efficiently
  • Reduce memory footprint of a large number of similar objects
  • Separate intrinsic (shared) state from extrinsic (unique) state
  • Allow many virtual objects to share the same data
  • Balance memory consumption and performance

Implementation

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

// Flyweight interface
interface Flyweight {
    void operation(String extrinsicState);
}

// Concrete Flyweight
class ConcreteFlyweight implements Flyweight {
    private final String intrinsicState;

    public ConcreteFlyweight(String intrinsicState) {
        this.intrinsicState = intrinsicState;
    }

    @Override
    public void operation(String extrinsicState) {
        System.out.println("Intrinsic State: " + intrinsicState);
        System.out.println("Extrinsic State: " + extrinsicState);
    }
}

// Flyweight Factory
class FlyweightFactory {
    private final Map<String, Flyweight> flyweights = new HashMap<>();

    public Flyweight getFlyweight(String key) {
        if (!flyweights.containsKey(key)) {
            flyweights.put(key, new ConcreteFlyweight(key));
            System.out.println("Creating new flyweight with key: " + key);
        } else {
            System.out.println("Reusing existing flyweight with key: " + key);
        }
        return flyweights.get(key);
    }

    public int getFlyweightCount() {
        return flyweights.size();
    }
}

// Client code
FlyweightFactory factory = new FlyweightFactory();

// Get flyweights with the same intrinsic state
Flyweight flyweight1 = factory.getFlyweight("shared");
Flyweight flyweight2 = factory.getFlyweight("shared");
Flyweight flyweight3 = factory.getFlyweight("different");

// Operations with different extrinsic states
flyweight1.operation("First external state");
flyweight2.operation("Second external state");
flyweight3.operation("Third external state");

System.out.println("Total flyweights created: " + factory.getFlyweightCount());

Text Editor Example

import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;

// Character Flyweight
class CharacterFlyweight {
    private final char character;
    private final String font;
    private final int size;

    public CharacterFlyweight(char character, String font, int size) {
        this.character = character;
        this.font = font;
        this.size = size;

        // Simulate memory consumption
        System.out.println("Creating flyweight for character: " + character + 
                          ", font: " + font + ", size: " + size);
    }

    public void display(int x, int y, String color) {
        System.out.println("Displaying character " + character + 
                          " at position (" + x + "," + y + ") with color " + color + 
                          " using font " + font + " and size " + size);
    }
}

// Flyweight Factory
class CharacterFlyweightFactory {
    private final Map<String, CharacterFlyweight> flyweights = new HashMap<>();

    public CharacterFlyweight getCharacter(char character, String font, int size) {
        String key = character + font + size;
        if (!flyweights.containsKey(key)) {
            flyweights.put(key, new CharacterFlyweight(character, font, size));
        }
        return flyweights.get(key);
    }

    public int getFlyweightCount() {
        return flyweights.size();
    }
}

// Text containing characters with position and color (extrinsic state)
class TextEditor {
    private final List<Character> characters = new ArrayList<>();
    private final CharacterFlyweightFactory factory;

    private int currentX = 0;
    private int currentY = 0;

    public TextEditor(CharacterFlyweightFactory factory) {
        this.factory = factory;
    }

    public void addCharacter(char character, String font, int size, String color) {
        CharacterFlyweight flyweight = factory.getCharacter(character, font, size);
        characters.add(new Character(flyweight, currentX, currentY, color));
        currentX += 10; // Move cursor position
    }

    public void display() {
        for (Character character : characters) {
            character.display();
        }
    }

    // Character position class (stores extrinsic state)
    private static class Character {
        private final CharacterFlyweight flyweight;
        private final int x;
        private final int y;
        private final String color;

        public Character(CharacterFlyweight flyweight, int x, int y, String color) {
            this.flyweight = flyweight;
            this.x = x;
            this.y = y;
            this.color = color;
        }

        public void display() {
            flyweight.display(x, y, color);
        }
    }
}

// Usage
CharacterFlyweightFactory factory = new CharacterFlyweightFactory();
TextEditor editor = new TextEditor(factory);

// Add text with the same font and size but different positions and colors
String text = "Hello Flyweight Pattern!";
for (int i = 0; i < text.length(); i++) {
    char c = text.charAt(i);
    String color = i % 2 == 0 ? "blue" : "red";
    editor.addCharacter(c, "Arial", 12, color);
}

editor.display();
System.out.println("Total flyweights created: " + factory.getFlyweightCount());
// Even though we added 23 characters, we only created unique flyweights for each character

Forest Rendering Example

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

// Tree type - intrinsic state
class TreeType {
    private final String name;
    private final String color;
    private final String texture;

    public TreeType(String name, String color, String texture) {
        this.name = name;
        this.color = color;
        this.texture = texture;

        System.out.println("Creating a tree type: " + name);
    }

    public void render(int x, int y) {
        System.out.println("Rendering " + name + " tree with " + color + 
                          " color and " + texture + " texture at (" + x + "," + y + ")");
    }
}

// Flyweight factory
class TreeFactory {
    private static final Map<String, TreeType> treeTypes = new HashMap<>();

    public static TreeType getTreeType(String name, String color, String texture) {
        String key = name + color + texture;
        if (!treeTypes.containsKey(key)) {
            treeTypes.put(key, new TreeType(name, color, texture));
        }
        return treeTypes.get(key);
    }

    public static int getTreeTypeCount() {
        return treeTypes.size();
    }
}

// Tree - contains extrinsic state
class Tree {
    private final int x;
    private final int y;
    private final TreeType type;

    public Tree(int x, int y, TreeType type) {
        this.x = x;
        this.y = y;
        this.type = type;
    }

    public void render() {
        type.render(x, y);
    }
}

// Forest - contains all trees
class Forest {
    private final List<Tree> trees = new ArrayList<>();

    public void plantTree(int x, int y, String name, String color, String texture) {
        TreeType type = TreeFactory.getTreeType(name, color, texture);
        Tree tree = new Tree(x, y, type);
        trees.add(tree);
    }

    public void render() {
        for (Tree tree : trees) {
            tree.render();
        }
    }
}

// Usage
Forest forest = new Forest();
Random random = new Random();

// Plant many trees of a few types
for (int i = 0; i < 100; i++) {
    int x = random.nextInt(500);
    int y = random.nextInt(500);

    String name;
    String color;
    String texture;

    if (random.nextBoolean()) {
        name = "Oak";
        color = "Green";
        texture = "Rough";
    } else {
        name = "Pine";
        color = "Dark Green";
        texture = "Smooth";
    }

    forest.plantTree(x, y, name, color, texture);
}

forest.render();
System.out.println("Total tree types created: " + TreeFactory.getTreeTypeCount());
// Even though we have 100 trees, we only created 2 tree types

When to Use the Flyweight Pattern

  • When an application uses a large number of objects
  • When memory usage is high due to the sheer quantity of objects
  • When the majority of each object's state can be made extrinsic (external)
  • When many objects can be replaced by a few shared objects
  • When the application doesn't depend on object identity
  • When the objects' intrinsic state is immutable or can be shared

Advantages

  • Reduces memory usage by sharing common data
  • Improves performance in memory-constrained environments
  • Decreases the total number of objects created
  • Makes it possible to represent large numbers of virtual objects without creating as many real ones
  • Keeps object instances to a minimum, which can improve responsiveness

Disadvantages

  • Adds complexity by separating intrinsic and extrinsic state
  • Managing extrinsic state can add runtime overhead
  • The pattern may not provide significant benefits if objects don't share significant amounts of data
  • Adds synchronization overhead when used in multi-threaded applications
  • Makes it harder to track individual object instances if they need to be tracked separately

Proxy Pattern

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. It creates a representative object that controls access to another object, which may be remote, expensive to create, or in need of securing.

Intent

  • Provide a surrogate or placeholder for another object to control access to it
  • Add a wrapper and delegation to protect the real component from undue complexity
  • Control access to the original object
  • Add functionality when accessing the original object
  • Delay the full cost of creating an object until we need to use it

Implementation

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

// Real Subject
class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("RealSubject: Handling request");
    }
}

// Proxy
class Proxy implements Subject {
    private RealSubject realSubject;

    @Override
    public void request() {
        // Lazy initialization: create the RealSubject only when needed
        if (realSubject == null) {
            realSubject = new RealSubject();
        }

        preRequest();
        realSubject.request();
        postRequest();
    }

    private void preRequest() {
        System.out.println("Proxy: Pre-processing request");
    }

    private void postRequest() {
        System.out.println("Proxy: Post-processing request");
    }
}

// Client code
Subject subject = new Proxy();
subject.request();

Types of Proxies

Virtual Proxy (Lazy Initialization)

// Heavy image resource
class Image {
    private String filename;

    public Image(String filename) {
        this.filename = filename;
        loadImageFromDisk();
    }

    private void loadImageFromDisk() {
        System.out.println("Loading image: " + filename);
        // Simulate loading a large image
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void display() {
        System.out.println("Displaying image: " + filename);
    }
}

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

// Real image implementation
class RealImage implements ImageInterface {
    private String filename;

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

    private void loadFromDisk() {
        System.out.println("Loading image: " + filename);
        // Simulate loading a large image
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

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

// Virtual proxy for the image
class VirtualImage implements ImageInterface {
    private String filename;
    private RealImage realImage;

    public VirtualImage(String filename) {
        this.filename = filename;
        System.out.println("Virtual image created: " + filename);
    }

    @Override
    public void display() {
        // Load the real image only when needed
        if (realImage == null) {
            realImage = new RealImage(filename);
        }
        realImage.display();
    }
}

// Usage
System.out.println("Creating image proxies...");
ImageInterface image1 = new VirtualImage("photo1.jpg");
ImageInterface image2 = new VirtualImage("photo2.jpg");

// The images aren't loaded until display() is called
System.out.println("Will display image1...");
image1.display(); // Now photo1.jpg is loaded

System.out.println("Will display image1 again...");
image1.display(); // No loading occurs, already loaded

System.out.println("Will display image2...");
image2.display(); // Now photo2.jpg is loaded

Protection Proxy (Access Control)

// Internet interface
interface Internet {
    void connectTo(String serverhost) throws Exception;
}

// Real Internet implementation
class RealInternet implements Internet {
    @Override
    public void connectTo(String serverHost) {
        System.out.println("Connecting to " + serverHost);
    }
}

// Protection proxy for controlling access
class RestrictedInternet implements Internet {
    private Internet internet = new RealInternet();
    private List<String> bannedSites;

    public RestrictedInternet() {
        bannedSites = Arrays.asList(
            "banned1.example.com",
            "banned2.example.com",
            "banned3.example.com"
        );
    }

    @Override
    public void connectTo(String serverHost) throws Exception {
        if (bannedSites.contains(serverHost)) {
            throw new Exception("Access Denied: Cannot connect to " + serverHost);
        }

        internet.connectTo(serverHost);
    }
}

// Usage
Internet internet = new RestrictedInternet();

try {
    internet.connectTo("allowed.example.com");      // Allowed
    internet.connectTo("banned1.example.com");      // Throws exception
} catch (Exception e) {
    System.out.println(e.getMessage());
}

Remote Proxy

// Remote interface
interface RemoteService {
    String performOperation(String data);
}

// Real service implementation (would be on a remote server)
class RealRemoteService implements RemoteService {
    @Override
    public String performOperation(String data) {
        return "Processed: " + data;
    }
}

// Remote proxy simulating RPC
class RemoteServiceProxy implements RemoteService {
    private RemoteService service;

    public RemoteServiceProxy() {
        // In a real scenario, this might set up network communication
        System.out.println("Setting up remote service connection");
    }

    @Override
    public String performOperation(String data) {
        // Lazy initialization of the service connection
        if (service == null) {
            System.out.println("Establishing connection to remote service");
            service = new RealRemoteService(); // In reality, this would be a remote reference
        }

        System.out.println("Sending data over the network: " + data);

        // Simulate network communication
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        String result = service.performOperation(data);

        System.out.println("Received result from remote service");
        return result;
    }
}

// Usage
RemoteService service = new RemoteServiceProxy();
String result = service.performOperation("Sample data");
System.out.println("Result: " + result);

Smart Proxy (Adding Functionality)

// Database connection interface
interface DatabaseExecutor {
    void execute(String query);
}

// Real database executor
class DatabaseExecutorImpl implements DatabaseExecutor {
    @Override
    public void execute(String query) {
        System.out.println("Executing query: " + query);
    }
}

// Smart proxy that adds transaction management and query logging
class DatabaseExecutorProxy implements DatabaseExecutor {
    private boolean isAdmin;
    private DatabaseExecutor executor;

    public DatabaseExecutorProxy(String user, String password) {
        if ("admin".equals(user) && "password".equals(password)) {
            isAdmin = true;
        }
        executor = new DatabaseExecutorImpl();
    }

    @Override
    public void execute(String query) {
        if (isAdmin) {
            // Add transaction management
            System.out.println("Starting transaction...");

            try {
                // Log the query
                logQuery(query);

                // Execute the query
                executor.execute(query);

                System.out.println("Committing transaction...");
            } catch (Exception e) {
                System.out.println("Rolling back transaction due to: " + e.getMessage());
            }
        } else {
            if (query.toLowerCase().startsWith("select")) {
                logQuery(query);
                executor.execute(query);
            } else {
                System.out.println("Access denied: Not authorized for non-select queries");
            }
        }
    }

    private void logQuery(String query) {
        System.out.println("Logging query: " + query);
    }
}

// Usage
DatabaseExecutor nonAdminExecutor = new DatabaseExecutorProxy("guest", "guest");
nonAdminExecutor.execute("SELECT * FROM users");            // Works
nonAdminExecutor.execute("DELETE FROM users WHERE id = 1"); // Access denied

DatabaseExecutor adminExecutor = new DatabaseExecutorProxy("admin", "password");
adminExecutor.execute("SELECT * FROM users");               // Works
adminExecutor.execute("DELETE FROM users WHERE id = 1");    // Works with transaction

When to Use the Proxy Pattern

  • When you need lazy initialization (virtual proxy)
  • When you need access control for the original object (protection proxy)
  • When you need to add logging, performance monitoring, or auditing (smart proxy)
  • When you need to connect to a remote service (remote proxy)
  • When you need a simplified version of a complex or heavy object (virtual proxy)
  • When you need a local representative for an object in a different address space (remote proxy)
  • When you need to cache results of expensive operations (caching proxy)

Advantages

  • Controls access to the original object
  • Allows operations to be performed before or after the request reaches the original object
  • Manages the lifecycle of the original object
  • Works even when the original object is not ready or not available
  • Adds functionality without changing the original object
  • Follows the Open/Closed Principle
  • Implements the principle of separation of concerns

Disadvantages

  • Adds another layer of indirection which can impact performance
  • Makes the code more complex
  • Client code might behave differently depending on whether it's using the proxy or the real subject
  • In some proxy implementations, the response might be delayed due to lazy initialization

Best Practices

General Best Practices for Structural Patterns

  1. Understand the Problem First: Choose a pattern only after you understand the problem you're trying to solve.

  2. Keep It Simple: Don't over-engineer solutions. Use the simplest pattern that meets your needs.

  3. Consider Combination: Sometimes combining patterns can provide more powerful solutions.

  4. Balance Performance: Structural patterns can introduce overhead, so ensure the benefits outweigh the costs.

  5. Document Your Design: Make sure to document why and how you're using specific patterns.

Pattern-Specific Best Practices

Adapter Pattern

  • Keep adapters simple and focused on interface translation
  • Consider using the Adapter pattern for legacy code integration
  • Use composition over inheritance when possible
  • Don't adapt more than necessary

Bridge Pattern

  • Identify the varying dimensions early
  • Design for independent evolution
  • Make sure abstractions and implementations can change independently
  • Consider future extensions to both hierarchies

Composite Pattern

  • Define clear component interface with operations for both leaf and composite
  • Consider whether to use the safe or transparent approach
  • Be careful with operations that don't make sense for particular components
  • Consider adding parent references if needed
  • Be explicit about whether clients can modify the component structure

Decorator Pattern

  • Keep decorator interface compatible with the component
  • Make decorators independent of each other
  • Keep the component code simple
  • Consider using Builder or Factory for complex decoration chains
  • Be consistent with method delegation

Facade Pattern

  • Keep facades simple and focused on providing a simplified interface
  • Avoid turning facades into "god objects"
  • Consider creating multiple facades for different use cases
  • Don't expose the subsystem components through the facade
  • Don't make facade dependent on client code

Flyweight Pattern

  • Clearly separate intrinsic and extrinsic state
  • Make flyweight objects immutable
  • Use a factory for flyweight creation and management
  • Ensure thread safety in the flyweight factory if needed
  • Consider the memory/performance tradeoff

Proxy Pattern

  • Keep the proxy interface identical to the subject
  • Choose the right type of proxy for your needs
  • Consider lazy initialization for expensive resources
  • Proxy should completely encapsulate access to the real subject
  • Don't add too much logic to the proxy

Common Pitfalls

Overusing Patterns

// Overusing patterns - unnecessary Facade
public class SimpleService {
    public void performOperation() {
        // Simple operation
    }
}

// Unnecessary Facade
public class ServiceFacade {
    private SimpleService service = new SimpleService();

    public void operation() {
        service.performOperation();
    }
}

// Better approach - use SimpleService directly if it's already simple

Overcomplicating with Unnecessary Layers

// Overcomplicating - too many decorators
interface Coffee {
    double getCost();
}

class BasicCoffee implements Coffee {
    @Override
    public double getCost() {
        return 2.0;
    }
}

class MilkDecorator implements Coffee {
    private Coffee coffee;

    public MilkDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

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

class SugarDecorator implements Coffee {
    private Coffee coffee;

    public SugarDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public double getCost() {
        return coffee.getCost() + 0.2;
    }
}

class WhippedCreamDecorator implements Coffee {
    private Coffee coffee;

    public WhippedCreamDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

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

class ChocolateSyrupDecorator implements Coffee {
    private Coffee coffee;

    public ChocolateSyrupDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public double getCost() {
        return coffee.getCost() + 0.8;
    }
}

class CreamDecorator implements Coffee {
    private Coffee coffee;

    public CreamDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public double getCost() {
        return coffee.getCost() + 0.3;
    }
}

// Too many decorators 
Coffee coffee = new ChocolateSyrupDecorator(
                new CreamDecorator(
                new WhippedCreamDecorator(
                new SugarDecorator(
                new MilkDecorator(
                new BasicCoffee())))));

// Better approach - combine related decorators or use a Builder pattern

Confusing Similar Patterns

Developers often confuse similar patterns:

  • Adapter vs. Bridge: Adapter makes incompatible interfaces work together; Bridge separates abstraction from implementation.
  • Decorator vs. Proxy: Decorator adds responsibilities; Proxy controls access.
  • Facade vs. Adapter: Facade provides a simplified interface; Adapter enables incompatible interfaces to work together.
  • Composite vs. Decorator: Composite builds tree structures; Decorator adds responsibilities without subclassing.

Performance Issues with Structural Patterns

// Performance issue - proxy with excessive overhead
class ExpensiveProxy implements Subject {
    private RealSubject realSubject;

    @Override
    public void request() {
        // Excessive overhead before delegation
        System.out.println("Starting proxy operation");
        for (int i = 0; i < 1000; i++) {
            // Unnecessary processing
            Math.sqrt(i);
        }

        // Create subject on every request instead of reusing
        realSubject = new RealSubject();

        realSubject.request();

        // Excessive overhead after delegation
        System.out.println("Finishing proxy operation");
        for (int i = 0; i < 1000; i++) {
            // Unnecessary processing
            Math.sqrt(i);
        }
    }
}

// Better approach - minimize overhead and cache the real subject
class EfficientProxy implements Subject {
    private RealSubject realSubject;

    @Override
    public void request() {
        // Lazy initialization with caching
        if (realSubject == null) {
            realSubject = new RealSubject();
        }

        // Minimal pre-processing
        System.out.println("Starting proxy operation");

        realSubject.request();

        // Minimal post-processing
        System.out.println("Finishing proxy operation");
    }
}

Ignoring Pattern Limitations

Each pattern has limitations that should be considered:

  • Adapter: Adds complexity and may hide poor design
  • Bridge: Increases complexity and isn't suitable for simple class hierarchies
  • Composite: May make design overly general and compromise type safety
  • Decorator: Can lead to many small, similar objects that are hard to debug
  • Facade: May turn into a god object if not designed carefully
  • Flyweight: Adds complexity for managing extrinsic state
  • Proxy: Adds indirection which can impact performance

Comparing Structural Patterns

When to Choose Which Pattern

Pattern When to Use
Adapter When you need to make incompatible interfaces work together
Bridge When you want to decouple an abstraction from its implementation
Composite When you need to work with tree-like object structures
Decorator When you want to add responsibilities dynamically without subclassing
Facade When you need to provide a simplified interface to a complex subsystem
Flyweight When you need to support a large number of fine-grained objects efficiently
Proxy When you need to control access to an object

Comparison of Key Characteristics

Pattern Intent Complexity Flexibility
Adapter Makes incompatible interfaces compatible Low-Medium Medium
Bridge Separates abstraction from implementation Medium-High High
Composite Treats individual objects and compositions uniformly Medium Medium
Decorator Adds responsibilities dynamically Medium High
Facade Simplifies a complex subsystem Low Low
Flyweight Shares common state between objects Medium-High Low
Proxy Controls access to an object Low-Medium Medium

Pattern Combinations

Structural patterns can be combined for more powerful solutions:

  1. Decorator + Composite: Decorators can add behavior to composite structures.
  2. Adapter + Bridge: Adapters can make bridges work with incompatible interfaces.
  3. Proxy + Flyweight: A proxy can manage reference to flyweight objects.
  4. Facade + Adapter: A facade can use adapters to work with incompatible subsystem components.
  5. Composite + Flyweight: Flyweights can be used to reduce memory usage in large composite structures.

Summary

Structural design patterns focus on how classes and objects are composed and provide solutions to create flexible, maintainable, and efficient code structure:

  • Adapter Pattern: Converts the interface of a class into another interface clients expect.
  • Bridge Pattern: Decouples an abstraction from its implementation.
  • Composite Pattern: Composes objects into tree structures to represent part-whole hierarchies.
  • Decorator Pattern: Adds responsibilities to objects dynamically without subclassing.
  • Facade Pattern: Provides a simplified interface to a complex subsystem.
  • Flyweight Pattern: Uses sharing to support large numbers of fine-grained objects efficiently.
  • Proxy Pattern: Provides a surrogate or placeholder for another object to control access.

Each pattern has specific use cases, advantages, and disadvantages. The key to effective use of structural patterns is understanding when and how to apply them to solve specific design problems.

By mastering these patterns, you'll be able to design more flexible, maintainable, and efficient object structures in your Java applications.

Further Reading