SerialReads

Delegation Pattern in Java

Apr 30, 2025

Great. I’ll prepare a comprehensive explanation of the Delegation Pattern in the context of Java. This will include:

Delegation Pattern in Java

Introduction: In this report, we explore the Delegation design pattern in Java, detailing its definition, design intent, key participants, and its differences from other behavioral patterns (notably the Observer and Strategy patterns). We also provide Java code examples to illustrate how delegation works in practice, and how it contrasts with Observer and Strategy in structure, purpose, communication flow, and usage scenarios.

1. What is the Delegation Pattern?

The Delegation Pattern is an object composition technique where one object hands off (delegates) a responsibility or behavior to a second object. Grady Booch described delegation as “a way to make composition as powerful for reuse as inheritance. In delegation, two objects are involved in handling a request: a receiving object delegates operations to its delegate.” (Delegation Pattern: An effective way of replacing Android’s Base Activity with native Kotlin support | by Prokash Sarkar | Medium) In simpler terms, one object (the delegator) relies on a helper object (the delegate) to perform a task or provide a result on its behalf (Delegation Pattern: An effective way of replacing Android’s Base Activity with native Kotlin support | by Prokash Sarkar | Medium). The delegator exposes a certain behavior externally, but internally it forwards the work to the delegate object.

Delegation vs. Other Patterns (High-Level): Unlike some other behavioral patterns which have very specific intents, delegation is a more fundamental design principle. It is not one of the original GoF (Gang of Four) design patterns, but rather a general approach that is used within many patterns (for example, the Strategy, Observer, Visitor, State, and others often use delegation under the hood (Delegation Pattern: An effective way of replacing Android’s Base Activity with native Kotlin support | by Prokash Sarkar | Medium)). The key idea is that instead of a single object doing everything, it delegates part of the work to another object – promoting better separation of concerns and lower coupling (Difference between Strategy pattern and Delegation pattern - Stack Overflow). This differs from patterns like Observer (which defines a one-to-many notification system) or Strategy (which defines interchangeable algorithms) that solve more specialized problems. We will delve deeper into these differences in later sections.

2. Design Intent and When to Use Delegation

The intent behind the delegation pattern is to achieve flexible code reuse and separation of concerns through composition rather than inheritance. By delegating tasks to helper objects, a class can incorporate new behaviors or responsibilities without needing to become a subclass of some base class. This design offers several benefits:

When to use the Delegation Pattern: You should consider delegation in scenarios where you want one object to forward certain work to another object, especially if you anticipate the behavior might change or have multiple implementations. Common use cases and triggers for using delegation include:

Benefits vs. Trade-offs: By using delegation, we get increased code reuse, flexibility, and a clear separation of concerns (no monolithic classes) (Delegation Pattern in Java: Mastering Efficient Task Assignment | Java Design Patterns). It also helps in testing (delegates can be mocked or replaced). However, there are trade-offs: it introduces additional layers of indirection (one more object involved in the call), which can add slight runtime overhead and complexity in understanding the flow. In practice, these downsides are usually minor compared to the design clarity gains, but they are worth noting. Overall, delegation is a powerful tool when used in the right contexts to keep designs modular and adaptable.

3. Key Participants in the Delegation Pattern

In a typical delegation scenario, there are three key participants or components involved:

(Delegation Pattern in Java: Mastering Efficient Task Assignment | Java Design Patterns) Sequence diagram of the Delegation pattern: The Client calls a method on the Delegator, which in turn forwards the request to a Delegate object. The Delegate performs the operation and returns the result to the Delegator, which then returns it to the Client.

In the sequence diagram above, the delegator acts as an intermediary between the client and the real work performed by the delegate. The client is unaware (and unconcerned) that the delegator is not doing the work itself – this is an implementation detail hidden by the delegator. The delegate could be swapped out for a different implementation without the client knowing, as long as the new delegate adheres to the expected interface.

Java Example: Delegation Pattern Implementation

Below is a simple Java example illustrating the delegation pattern. In this example, we have a Printer interface (the delegate interface) with a print method. We provide two concrete implementations of Printer (e.g., CanonPrinter and EpsonPrinter – the delegates), each printing a message in a device-specific way. We also have a PrinterController class (the delegator) that implements Printer but actually delegates the printing task to an internal Printer instance:

// Delegate interface
interface Printer {
    void print(String message);
}

// Concrete Delegate A
class CanonPrinter implements Printer {
    public void print(String message) {
        System.out.println("Canon Printer: " + message);
    }
}

// Concrete Delegate B
class EpsonPrinter implements Printer {
    public void print(String message) {
        System.out.println("Epson Printer: " + message);
    }
}

// Delegator class that implements the interface by delegating to a helper
class PrinterController implements Printer {
    private Printer delegate;  // holds a reference to a Printer (delegate)

    public PrinterController(Printer delegate) {
        this.delegate = delegate;
    }
    public void setDelegate(Printer delegate) {
        this.delegate = delegate;  // allow changing the delegate at runtime
    }

    @Override
    public void print(String message) {
        // Forward the request to the delegate object
        delegate.print(message);
    }
}

In the code above, PrinterController doesn’t implement the details of printing itself; it simply forwards the call to whatever Printer implementation it currently holds. This means PrinterController can be treated as a Printer (it fulfills the interface), but it’s flexible – the actual printing behavior depends on the delegate it’s given.

Now, consider using these classes:

PrinterController controller = new PrinterController(new CanonPrinter());
controller.print("Hello World");   // Outputs: "Canon Printer: Hello World"

controller.setDelegate(new EpsonPrinter());
controller.print("Hello World");   // Outputs: "Epson Printer: Hello World"

Here, at first the controller delegates to a CanonPrinter. Later, we swap the delegate to an EpsonPrinter at runtime. The PrinterController itself didn’t need to change; it continues to accept print requests and delegates them. This demonstrates how delegation allows changing behavior dynamically (composition instead of hard-coded inheritance). The client code simply interacts with PrinterController via the Printer interface, and is unaware of which concrete printer actually handles the call.

4. Delegation Pattern vs. Observer Pattern

The Observer Pattern (also known as Publish-Subscribe or Listener pattern) is another behavioral design pattern, but it serves a different purpose than delegation. Observer defines a one-to-many relationship between objects so that when one object’s state changes, all its dependents (observers) are notified automatically (Observer Pattern) (Observer). In Java, the observer pattern is often implemented with a Subject (or Publisher) class maintaining a list of observers and notifying them of events. For example, a Subject might have methods like registerObserver(), removeObserver(), and notifyObservers() to manage the list of observers and broadcast changes.

Key differences between Delegation and Observer:

Delegate as a specialized Observer: It’s worth noting that a delegate in the sense of a “single observer” can be viewed as a special case of the observer pattern with exactly one observer. In practice, the patterns are distinguished by intent: a delegate usually implies the delegate is taking on a responsibility (often controlling some aspect of the delegator’s behavior), whereas an observer is simply being informed of something that happened, with the subject not caring what the observer does with that information (What are the advantages of the delegate pattern over the observer pattern? - Software Engineering Stack Exchange) (What are the advantages of the delegate pattern over the observer pattern? - Software Engineering Stack Exchange). The delegate often has a closer, more specific role in the operation of the delegator (sometimes even able to influence it), whereas observers are kept at arm’s length (just notified of changes).

Java Example: Observer Pattern Implementation

To illustrate the observer pattern in Java, consider a simple news agency example. We have a NewsAgency class (the Subject) which maintains a list of observers interested in news updates. Observers implement a Channel interface (with an update method). When the news agency gets new news, it notifies all registered channels by calling their update method:

// Observer interface
interface Channel {
    void update(String news);
}

// Concrete Observer
class NewsChannel implements Channel {
    private String news;
    @Override
    public void update(String news) {
        this.news = news;
        System.out.println("[NewsChannel] Breaking News: " + news);
    }
}

// Subject class
class NewsAgency {
    private List<Channel> channels = new ArrayList<>();
    private String latestNews;

    public void addObserver(Channel channel) {
        channels.add(channel);
    }
    public void removeObserver(Channel channel) {
        channels.remove(channel);
    }

    public void setLatestNews(String news) {
        this.latestNews = news;
        notifyObservers();
    }
    private void notifyObservers() {
        for (Channel channel : channels) {
            channel.update(latestNews);
        }
    }
}

In this code, NewsAgency is the subject that keeps track of subscribed Channel observers. Whenever setLatestNews is called, it goes through the list of channels and calls update(...) on each. The NewsChannel observer simply prints the news (in a real scenario it might update a display, etc.). We can use this as follows:

NewsAgency agency = new NewsAgency();
NewsChannel channel1 = new NewsChannel();
NewsChannel channel2 = new NewsChannel();
agency.addObserver(channel1);
agency.addObserver(channel2);

agency.setLatestNews("New Java release 18.0.3");  
// Both channel1 and channel2 will receive the update and print the news.

In this observer example, the NewsAgency does not know or care what the NewsChannel observers do with the news; it simply notifies all of them. This is fundamentally different from delegation where there would be just one delegate (e.g., if NewsAgency had a single Channel delegate, it would delegate the act of handling news to that one channel rather than broadcasting to many).

5. Delegation Pattern vs. Strategy Pattern

The Strategy Pattern is another behavioral pattern that, at first glance, looks very similar to delegation. Strategy also involves one object (often called the Context) holding a reference to another object (the Strategy) which implements a certain interface, and the context delegates a task to it. The key difference is the intent: the strategy pattern’s purpose is to encapsulate interchangeable algorithms and allow the client or context to choose which algorithm to use at runtime (Strategy pattern - Wikipedia) (Strategy). In other words, Strategy is about selecting how to do something from a family of algorithms, whereas Delegation (in general) is about assigning responsibility to a helper object. Let’s break down the differences:

Java Example: Strategy Pattern Implementation

To clarify the strategy pattern, here’s a Java example. We’ll implement a simple calculator that can use different strategies to perform an operation on two numbers. The strategy interface will define a single method for executing an operation, and we’ll have two concrete strategies: one for addition and one for multiplication. The context class Calculator will have a strategy and delegate the computation to it:

// Strategy interface
interface OperationStrategy {
    int execute(int a, int b);
}

// Concrete Strategy 1: Addition
class AdditionStrategy implements OperationStrategy {
    public int execute(int a, int b) {
        return a + b;
    }
}
// Concrete Strategy 2: Multiplication
class MultiplicationStrategy implements OperationStrategy {
    public int execute(int a, int b) {
        return a * b;
    }
}

// Context class that uses a strategy
class Calculator {
    private OperationStrategy strategy;
    public Calculator(OperationStrategy strategy) {
        this.strategy = strategy;
    }
    public void setStrategy(OperationStrategy strategy) {
        this.strategy = strategy;
    }
    public int compute(int x, int y) {
        return strategy.execute(x, y);  // delegate computation to strategy
    }
}

Using this Calculator context with different strategies:

Calculator calc = new Calculator(new AdditionStrategy());
System.out.println(calc.compute(3, 4));  // Outputs: 7  (uses addition)

calc.setStrategy(new MultiplicationStrategy());
System.out.println(calc.compute(3, 4));  // Outputs: 12 (now uses multiplication)

In this example, the Calculator initially delegates the compute operation to an addition strategy. Later, we swap in a multiplication strategy. This is clearly the Strategy pattern in action – we have a family of algorithms (add, multiply, etc.) encapsulated as strategy objects, and the context can switch between them. Internally, this works exactly via delegation (the Calculator calls the execute method of whatever OperationStrategy it holds). The difference from a generic delegation example is mainly semantic: we identify this as “Strategy pattern” because the focus is on interchangeable algorithms and runtime switching. If the Calculator never intended to change strategies (say it always used addition passed in via constructor and never changed it), one could argue it’s just using delegation to perform the operation. The ability to change it is what makes it a clear Strategy usage.

6. Summary of Differences

To summarize the differences between Delegation, Observer, and Strategy in Java:

In practice, all three involve abstraction and indirection to promote flexibility and decoupling. Delegation as a principle underpins many design patterns (including Strategy and certain implementations of Observer). An advanced Java developer will recognize that these patterns are tools in a toolbox: Observer for event systems, Strategy for algorithm selection, and Delegation for structuring collaborations between objects in a flexible way. By understanding these patterns and the nuances in their structure and intent, one can choose the right approach for a given design problem and write code that is maintainable, extensible, and clear.

Sources:

design-patterns