Loading learning content...
In the previous modules, we established that composition is often preferable to inheritance for building flexible, maintainable systems. But composition alone is just a structural relationship—objects containing other objects. The real power emerges when we understand delegation: the dynamic mechanism by which composed objects actually collaborate.
Delegation is the act of one object forwarding a request to another object to handle. Rather than implementing behavior directly, an object entrusts that responsibility to a collaborator. This simple concept is surprisingly powerful, enabling runtime flexibility, clean separation of concerns, and elegant designs that inheritance simply cannot achieve.
If composition is the structure of "has-a" relationships, delegation is the behavior of those relationships—the protocol by which objects work together.
By the end of this page, you will understand what delegation is, how it differs from simple method forwarding, its relationship to composition, and the mental model for thinking about delegating objects. You'll see delegation as the behavioral essence of composition—the mechanism that brings "has-a" relationships to life.
Delegation is a design technique where an object handles a request by forwarding it to a second object (the delegate) for processing. The first object doesn't implement the behavior itself; instead, it relies on its delegate to provide the implementation.
This concept is fundamental to object-oriented design, yet it's often overlooked in favor of discussing inheritance. Let's establish a precise definition:
Definition: Delegation occurs when an object (the delegator) receives a message and responds by sending one or more messages to another object (the delegate), which performs the actual work. The delegator may then return the delegate's result, transform it, or combine it with other operations.
The Key Insight: Delegation is not just "calling another object's method." It's a design decision to assign responsibility to a collaborating object rather than implementing behavior internally.
| Term | Definition | Role |
|---|---|---|
| Delegator | The object receiving the original request | Forwards work to the delegate |
| Delegate | The object that performs the actual work | Implements the delegated behavior |
| Delegation | The act of forwarding responsibility | The mechanism of collaboration |
| Protocol/Interface | The contract between delegator and delegate | Defines what can be delegated |
A Concrete Example
Consider a Printer class that needs to format documents before printing. Rather than implementing formatting logic itself, it delegates to a DocumentFormatter:
Printer receives a print requestDocumentFormatter delegateDocumentFormatter returns the formatted outputPrinter then handles the actual printingThe Printer has a DocumentFormatter (composition) and uses it by delegating formatting responsibility (delegation).
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// The Delegate Interface - defines what can be delegatedinterface DocumentFormatter { format(content: string): string;} // A concrete delegate implementationclass MarkdownFormatter implements DocumentFormatter { format(content: string): string { // Transform markdown to formatted output return this.parseMarkdown(content); } private parseMarkdown(content: string): string { // Markdown parsing logic return content .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') .replace(/\*(.+?)\*/g, '<em>$1</em>'); }} // The Delegator - forwards formatting to its delegateclass Printer { // Composition: Printer HAS-A DocumentFormatter private formatter: DocumentFormatter; constructor(formatter: DocumentFormatter) { this.formatter = formatter; } print(document: string): void { // DELEGATION: Forward formatting responsibility to delegate const formattedContent = this.formatter.format(document); // Printer handles its own responsibility: printing this.sendToDevice(formattedContent); } private sendToDevice(content: string): void { console.log('Printing:', content); }} // Usageconst printer = new Printer(new MarkdownFormatter());printer.print("**Hello** *World*");// Output: Printing: <strong>Hello</strong> <em>World</em>Notice how delegation creates a natural separation of concerns. The Printer focuses on printing—its core responsibility. Formatting knowledge is encapsulated in the formatter. Neither class needs to understand the other's internal workings. They collaborate through a well-defined interface.
A common confusion arises: "Isn't delegation just calling another object's method?" While delegation involves method calls, it's conceptually distinct. Understanding this difference is crucial for applying delegation as a design pattern rather than an accidental implementation detail.
Simple Method Call: Any object invoking a method on another object. This is just the mechanics of OOP—objects sending messages.
Delegation as Design Pattern: A deliberate architectural decision where an object's behavior is intentionally implemented by forwarding to a collaborating object that can vary independently.
The difference lies in intent and design:
Illustrating the Distinction
Consider two approaches to implementing a Logger that formats log messages:
Simple Method Call (Not True Delegation):
class Logger {
private helper = new MessageHelper(); // Hardcoded dependency
log(message: string): void {
// Just using a helper—no intentional design for variation
const formatted = this.helper.addTimestamp(message);
console.log(formatted);
}
}
True Delegation Pattern:
class Logger {
constructor(private formatter: MessageFormatter) {} // Injected interface
log(message: string): void {
// Intentional delegation—formatter is an extension point
const formatted = this.formatter.format(message);
console.log(formatted);
}
}
In the first case, MessageHelper is an implementation detail. In the second, MessageFormatter is an intentional extension point. The Logger expects different formatters to be plugged in—that's the design intent.
Delegation as a pattern is about strategic decoupling. When you design with delegation, you're saying: "This behavior is not my core responsibility. It could vary. I intentionally defer to a collaborator who specializes in this." This intent guides you to use interfaces, accept delegates through constructors, and think about what variations might be needed.
Composition and delegation are deeply intertwined, but they describe different aspects of object relationships:
You cannot have delegation without some form of composition (the delegator must hold a reference to the delegate). But you can have composition without delegation (an object might contain another object but never ask it to do work).
Composition + Delegation = Flexible Behavior Reuse
The power emerges when you combine them deliberately:
Together, they enable the kind of flexible, runtime-reconfigurable behavior that inheritance cannot provide.
| Aspect | Composition | Delegation | Inheritance |
|---|---|---|---|
| Nature | Structural relationship | Behavioral protocol | Type relationship |
| Question Answered | What does this object contain? | How do objects collaborate? | What type is this object? |
| Binding | Can be runtime | Can be runtime | Compile-time only |
| Coupling | To interface (loose) | To protocol (loose) | To implementation (tight) |
| Flexibility | High (swap components) | High (swap behavior) | Low (fixed hierarchy) |
| Reuse Mechanism | Assembly | Forwarding | Inheriting |
The "Favor Composition Over Inheritance" Principle Revisited
Recall the famous Gang of Four principle. Now we can understand it more precisely:
"Favor composition and delegation over class inheritance."
The principle isn't just about containing objects—it's about using those contained objects to implement behavior that would otherwise require inheritance. Delegation is the mechanism by which composition replaces inheritance.
Example: Replacing Inheritance with Composition + Delegation
Inheritance approach:
class Duck extends FlyingAnimal { }
Composition + Delegation approach:
class Duck {
constructor(private flyBehavior: FlyBehavior) { }
fly() { this.flyBehavior.fly(); } // Delegation
}
In the second approach, Duck HAS a fly behavior (composition) and USES it when asked to fly (delegation). Different ducks can have different fly behaviors, and even the same duck can change its fly behavior at runtime.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// The Strategy interface - defines a family of behaviorsinterface FlyBehavior { fly(): string;} // Concrete strategies - different implementationsclass StandardFlying implements FlyBehavior { fly(): string { return "Flying with wings!"; }} class JetPoweredFlying implements FlyBehavior { fly(): string { return "Flying with jet propulsion!"; }} class NoFlying implements FlyBehavior { fly(): string { return "Sorry, I can't fly."; }} // Duck uses COMPOSITION to hold a FlyBehavior// Duck uses DELEGATION to implement fly()class Duck { // COMPOSITION: Duck has-a FlyBehavior private flyBehavior: FlyBehavior; constructor(flyBehavior: FlyBehavior) { this.flyBehavior = flyBehavior; } // DELEGATION: Duck forwards fly() to its FlyBehavior performFly(): string { return this.flyBehavior.fly(); } // Runtime flexibility: behavior can be changed! setFlyBehavior(newBehavior: FlyBehavior): void { this.flyBehavior = newBehavior; }} // Usage demonstrates the power of composition + delegationconst mallard = new Duck(new StandardFlying());console.log(mallard.performFly()); // "Flying with wings!" const rubberDuck = new Duck(new NoFlying());console.log(rubberDuck.performFly()); // "Sorry, I can't fly." // Runtime behavior change—impossible with inheritance!mallard.setFlyBehavior(new JetPoweredFlying());console.log(mallard.performFly()); // "Flying with jet propulsion!"Composition provides the container. Delegation provides the collaboration protocol. Interfaces provide the abstraction for variation. Together, they create systems where behavior is assembled from interchangeable parts rather than hardcoded into hierarchies.
To effectively apply delegation in your designs, it helps to develop a clear mental model. Here are several useful ways to think about delegation:
Mental Model 1: The Expert Colleague
Think of delegation like working with a specialist colleague. You're a project manager (the delegator). When a task requires legal expertise, you don't try to learn law—you forward the task to the legal expert (the delegate). They handle it and give you the result.
Mental Model 2: The Configurable Machine
Imagine a coffee machine with interchangeable grinder modules. The machine (delegator) has a slot for a grinder (delegate). Different grinders produce different grinds. The machine doesn't need to know about burr grinders vs blade grinders—it just says "grind coffee" and uses whatever grinder is installed.
Mental Model 3: The Orchestra Conductor
An orchestra conductor (delegator) doesn't play every instrument. They coordinate, giving cues to different specialists (delegates)—the violinist, the percussionist, the flutist. Each musician knows their craft; the conductor knows when and how to combine their contributions.
This model is particularly relevant when objects delegate to multiple collaborators:
class OrderProcessor {
constructor(
private paymentGateway: PaymentGateway,
private inventoryService: InventoryService,
private notificationService: NotificationService
) {}
processOrder(order: Order): void {
// Delegate payment processing
this.paymentGateway.charge(order.total);
// Delegate inventory update
this.inventoryService.reserve(order.items);
// Delegate notification
this.notificationService.sendConfirmation(order);
}
}
The OrderProcessor is the conductor, coordinating the work of three specialized delegates.
Ask yourself: "Am I implementing this behavior, or am I coordinating someone else to do it?" If you find yourself writing code that's not the core responsibility of the class, consider delegation. The class that receives requests doesn't always have to be the one that fulfills them.
Delegation manifests in several forms, each with different characteristics. Understanding these variations helps you choose the right approach for your design:
1. Simple Forwarding
The delegator receives a request and forwards it directly to the delegate, returning the result unchanged:
class Printer {
print(doc: Document): void {
this.formatter.format(doc); // Forward
}
}
2. Decorated Forwarding
The delegator adds behavior before or after delegating:
class LoggingPrinter {
print(doc: Document): void {
this.logger.log('Before print');
this.innerPrinter.print(doc); // Delegate
this.logger.log('After print');
}
}
3. Coordinating Delegation
The delegator coordinates multiple delegates to fulfill a request:
class ReportGenerator {
generate(): Report {
const data = this.dataFetcher.fetch();
const analyzed = this.analyzer.analyze(data);
return this.renderer.render(analyzed);
}
}
| Pattern | Description | Use Case | Example |
|---|---|---|---|
| Simple Forwarding | Pass request directly to delegate | Single responsibility delegation | Printer → Formatter |
| Decorated Forwarding | Add behavior around delegation | Cross-cutting concerns (logging, caching) | CachingRepository → Repository |
| Coordinating | Orchestrate multiple delegates | Complex workflows | OrderProcessor → multiple services |
| Conditional Delegation | Choose delegate based on context | Polymorphic selection | PaymentProcessor → card/bank delegate |
| Chain Delegation | Delegate until one handles it | Handler chain scenarios | Chain of Responsibility pattern |
4. Conditional Delegation
The delegator selects which delegate to forward to based on the context:
class PaymentProcessor {
processPayment(payment: Payment): void {
if (payment.type === 'card') {
this.cardGateway.process(payment);
} else {
this.bankGateway.process(payment);
}
}
}
Note: This can often be improved using polymorphism:
class PaymentProcessor {
process(payment: Payment): void {
const gateway = this.gatewayFactory.getGateway(payment.type);
gateway.process(payment); // Polymorphic delegation
}
}
5. Self-Referential Delegation
In true delegation (as in prototype-based languages like JavaScript), the delegate can refer back to the original object. This allows the delegate to use the delegator's state or call its methods. Most class-based languages don't directly support this, but it can be simulated:
interface Delegate {
doWork(context: Delegator): void;
}
class SomeDelegate implements Delegate {
doWork(context: Delegator): void {
// Can access the delegator's state!
console.log(context.name);
}
}
In academic literature, "true delegation" specifically means the delegate can reference the original receiver (self). In class-based languages like Java/TypeScript, what we typically call delegation is technically "forwarding" or "consultation." In practice, the terms are used interchangeably, and the design benefits apply regardless of the technical distinction.
Delegation is so fundamental that it appears as the core mechanism in many well-known design patterns. Recognizing these patterns helps you see delegation's broad applicability:
Strategy Pattern
The Strategy pattern is perhaps the purest example of delegation. An object delegates an algorithm to a strategy object:
PaymentProcessor delegates payment algorithm to PaymentStrategySorter delegates sorting algorithm to SortingStrategyCompressor delegates compression algorithm to CompressionStrategyThe delegator maintains a reference to a strategy interface and forwards algorithmic requests to it.
Decorator Pattern
Decorators use delegation to wrap objects with additional behavior:
The decorator and the decorated object share the same interface, enabling transparent wrapping.
Composite Pattern
Composite uses delegation to implement tree-structured hierarchies:
State Pattern
The State pattern delegates behavior to an object representing current state:
Document delegates behavior to its DocumentStateObserver Pattern (Event Listeners)
Even event systems use delegation:
When studying design patterns, notice how many are essentially different recipes for delegation. The Strategy pattern says "delegate the algorithm." The State pattern says "delegate to current state." The Decorator says "delegate with added behavior." Understanding delegation deeply makes these patterns intuitive.
Having established what delegation is and how it manifests, let's consolidate its benefits. These advantages explain why delegation is a cornerstone of flexible object-oriented design:
MarkdownFormatter can be used by any class that needs markdown formatting.The Testing Advantage Illustrated
Consider testing a NotificationService that delegates to an EmailSender:
// Production code
class NotificationService {
constructor(private emailSender: EmailSender) {}
notify(user: User, message: string): void {
this.emailSender.send(user.email, message);
}
}
// Test code - easy to mock!
it('should send email to user', () => {
const mockEmailSender = {
send: jest.fn() // Mock the delegate
};
const service = new NotificationService(mockEmailSender);
service.notify({ email: 'test@example.com' }, 'Hello');
expect(mockEmailSender.send).toHaveBeenCalledWith('test@example.com', 'Hello');
});
Without delegation, the NotificationService might instantiate its email sender internally, making testing much harder.
These benefits compound. Loose coupling enables testability. Testability encourages better design. Single responsibility makes code clearer. Clarity reduces bugs. Runtime flexibility allows rapid iteration. Each benefit reinforces the others, creating a virtuous cycle of design quality.
We've established a comprehensive understanding of what delegation is and why it matters. Let's consolidate the key insights:
What's Next:
Now that we understand what delegation is conceptually, the next page examines the mechanics in detail: Forwarding Requests to Contained Objects. We'll explore the implementation patterns, the relationship between the delegator and delegate, and how to set up effective delegation in practice.
You now understand delegation as a fundamental design technique—the behavioral mechanism that brings composition to life. Delegation is not just calling methods; it's a deliberate architectural choice for flexibility. Next, we'll explore how to implement effective delegation by examining the mechanics of forwarding requests to contained objects.