Advantage of Decorator over inheritance
The Decorator pattern (also known as Wrapper) is a Structural design pattern that allows developers to dynamically attach new behaviors and responsibilities to objects at runtim...
🟢 Junior Level
The Decorator pattern (also known as Wrapper) is a Structural design pattern that allows developers to dynamically attach new behaviors and responsibilities to objects at runtime without altering their underlying source code and without relying on class inheritance.
The Flaw of Inheritance: Combinatorial Class Explosion
Consider a beverage pricing system where customers can customize coffee with optional add-ons (milk, sugar, syrup, whipped cream). If implemented using class inheritance, every possible permutation requires its own distinct subclass:
Inheritance Hierarchy:
Coffee
┌───────────┼───────────┐
MilkCoffee SugarCoffee SyrupCoffee
┌───────────┴───────────┐
MilkSugarCoffee MilkSyrupCoffee
│
MilkSugarSyrupCoffee (Requires 2^N subclasses for N toppings!)
With 10 optional add-ons, inheritance mandates $2^{10} = 1024$ classes, causing an unmaintainable codebase.
The Decorator Solution: Object Composition
Instead of subclassing, Decorator uses object composition. The decorator implements the same interface as the wrapped object and holds an internal reference to it:
import java.math.BigDecimal;
// 1. Common Component Interface
public interface Beverage {
String getDescription();
BigDecimal getCost();
}
// 2. Base Component
public class Espresso implements Beverage {
public String getDescription() { return "Espresso"; }
public BigDecimal getCost() { return BigDecimal.valueOf(2.50); }
}
// 3. Base Decorator
public abstract class BeverageDecorator implements Beverage {
protected final Beverage wrapped;
public BeverageDecorator(Beverage wrapped) {
this.wrapped = wrapped;
}
}
// 4. Concrete Decorators
public class MilkDecorator extends BeverageDecorator {
public MilkDecorator(Beverage wrapped) { super(wrapped); }
public String getDescription() { return wrapped.getDescription() + ", Milk"; }
public BigDecimal getCost() { return wrapped.getCost().add(BigDecimal.valueOf(0.50)); }
}
public class SugarDecorator extends BeverageDecorator {
public SugarDecorator(Beverage wrapped) { super(wrapped); }
public String getDescription() { return wrapped.getDescription() + ", Sugar"; }
public BigDecimal getCost() { return wrapped.getCost().add(BigDecimal.valueOf(0.20)); }
}
// Client usage: Nesting decorators at runtime without class explosion!
Beverage order = new SugarDecorator(new MilkDecorator(new Espresso()));
System.out.println(order.getDescription() + " = $" + order.getCost());
// Output: Espresso, Milk, Sugar = $3.20
🟡 Middle Level
Architectural Comparison: Inheritance vs Decorator
| Dimension | Class Inheritance | Decorator Pattern |
|---|---|---|
| Binding Mechanism | Static (Compile-time): Behavior is permanently baked into class bytecode. | Dynamic (Runtime): Behavior is assembled dynamically while the program executes. |
| Class Count Growth | Exponential ($2^N$): Combinatorial explosion for multiple optional features. | Linear ($N$): One concise decorator class per optional feature. |
| Coupling Level | High: Subclasses depend heavily on parent implementation details. | Low: Decorators depend exclusively on an abstract interface. |
| SOLID Compliance | Violates Single Responsibility (subclasses mix multiple responsibilities). | Adheres to SRP (one responsibility per decorator) and OCP. |
| Runtime Mutability | Cannot alter or remove behaviors from an existing instance. | Can wrap, reorder, or peel layers dynamically. |
The Canonical Standard: java.io Streams
The Java Standard Library’s I/O package is the quintessential real-world demonstration of the Decorator pattern:
InputStream stream = new BufferedInputStream( // Layer 3: Adds 8KB memory buffer
new GZIPInputStream( // Layer 2: On-the-fly decompression
new FileInputStream("data.gz") // Layer 1: Reads raw disk bytes
)
);
FileInputStreamis the concrete component reading raw bytes from storage.GZIPInputStreamis a decorator adding decompression.BufferedInputStreamis a decorator adding memory caching. Clients consume the stream polymorphically through the baseInputStreaminterface without coupling to file systems, compression algorithms, or buffering strategies.
The Structural Triad: Decorator vs Proxy vs Adapter
| Pattern | Primary Intent | Interface Status | Lifecycle Management |
|---|---|---|---|
| Decorator | Adds new behaviors & responsibilities dynamically. | Preserves identical interface. | Client typically constructs the wrapper chain. |
| Proxy | Controls / restricts access (lazy load, auth, caching). | Preserves identical interface. | Proxy typically manages the target’s lifecycle internally. |
| Adapter | Bridges incompatible APIs and converts contracts. | Changes interface to target contract. | Client interacts with new adapted interface. |
🔴 Senior Level
“Composition over Inheritance” and the Fragile Base Class
Joshua Bloch (Effective Java, Item 18: “Favor composition over inheritance”) highlights the fundamental architectural perils of inheritance:
- Encapsulation Breach: A subclass is tied to the internal execution details of its superclass. If an updated superclass alters how internal methods call each other (self-use), subclass behavior can silently break without compilation errors.
- Fragile Base Class: Adding a new method to a base class can inadvertently break subclasses that coincidentally share that method name or rely on internal state invariants.
- Decorator Resolution: The Decorator pattern treats the wrapped component as an opaque black box, interacting strictly through its public interface. Changes to the wrapped class’s internal implementation details cannot corrupt the decorator.
The “SELF” Problem (Self-Invocation Bypass)
A notorious pitfall of the Decorator pattern is the SELF Problem:
- The wrapped component has no reference to the outer decorator.
- If method
A()in the base component calls methodB()internally viathis.B(), the call executes within the base component, completely bypassing any decorators that overrideB().
public class BaseService implements Service {
public void execute() {
this.logAction(); // Invokes local method, NOT the LoggingDecorator!
}
public void logAction() { System.out.println("Default log"); }
}
Object Identity Crisis
Decorators introduce identity hazards in Java collections:
- The decorator and the wrapped object are distinct instances in the JVM heap (
decorator != original). - If client code relies on reference equality (
==),System.identityHashCode(), orMap.containsKey(original)after passing the decorated instance into a subsystem, subtle equality bugs emerge.
Enterprise Usage: Servlet Filters and Spring AOP
1. Servlet Request Wrappers (HttpServletRequestWrapper)
In enterprise web filters, inspecting or altering incoming HTTP request bodies requires the Decorator pattern:
public class DecryptingRequestWrapper extends HttpServletRequestWrapper {
public DecryptingRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public ServletInputStream getInputStream() throws IOException {
// Return custom stream that decrypts payload bytes on the fly
return new DecryptingServletInputStream(super.getInputStream());
}
}
2. Spring AOP as a Declarative Meta-Decorator
Spring annotations like @Transactional, @Cacheable, and @Retryable act as declarative decorators. Behind the scenes, Spring creates dynamic proxies (via CGLIB or JDK Dynamic Proxies) that wrap beans with interceptors, weaving cross-cutting concerns around business methods.
4 Tricky Questions
1. How does the Decorator pattern resolve the Fragile Base Class problem inherent in inheritance?
Answer: In class inheritance, subclasses depend on the internal implementation details and private execution semantics of their parent class (e.g., self-use of internal methods). If the base class changes its internal implementation, subclasses can silently break. The Decorator pattern resolves this by treating the target object as a black box accessed strictly through an abstract interface. The decorator delegates calls without assuming anything about the component’s internal state or private methods, preventing base class modifications from cascading into wrapper failures.
2. What is the “SELF Problem” (Self-Invocation Bypass) in the Decorator pattern, and what subtle bugs can it introduce?
Answer: The “SELF Problem” occurs because the decorated object maintains no reference to the outer wrapper object that encloses it. When a method inside the decorated component invokes another method on itself using the implicit this reference (e.g., this.helperMethod()), the call executes directly against the original object rather than being routed through the decorator’s overridden version of helperMethod(). This self-invocation bypasses security checks, caching, or logging added by the decorator, leading to silent functional omissions.
3. How do Decorator and Proxy differ if both implement the exact same interface and maintain a reference to the target object?
Answer: The distinction lies in their intent and lifecycle management:
- Decorator: Its primary intent is to add new business functionality or behaviors dynamically. The client code typically controls the assembly of the decorator chain (e.g., wrapping a stream in several layers).
- Proxy: Its primary intent is to control access to the target object (e.g., lazy loading, security authorization, remote RPC calls, caching). The proxy typically creates or manages the target object’s lifecycle internally, concealing the concrete target from the client entirely.
4. Why is HttpServletRequestWrapper in the Java Servlet API designed as a Decorator rather than an Adapter?
Answer: An Adapter pattern is used to convert an existing interface into a different, incompatible target interface required by a client. In contrast, HttpServletRequestWrapper implements the exact same HttpServletRequest interface as the container’s native request object. It is designed to intercept and override specific methods (such as getParameter() or getInputStream()) while delegating all other methods unchanged to the wrapped underlying request. This matches the exact definition and purpose of the Decorator pattern.
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“The Decorator pattern allows adding new functionality to an object dynamically at runtime using object composition instead of static compile-time inheritance. It resolves the combinatorial class explosion problem, reducing $2^N$ subclass variations to $N$ independent, reusable decorator classes. Classic examples include Java’s
java.iostream hierarchy (BufferedInputStreamwrappingFileInputStream), ServletHttpServletRequestWrapper, and Spring AOP proxies that weave transactional and caching concerns around beans.”
Key Architectural Takeaways
- Composition over Inheritance: Dynamic runtime assembly over static compile-time subclassing.
- Class Explosion Prevention: Eliminates the need for $2^N$ subclasses for combinations of $N$ features.
- Same Interface: Decorator implements the exact interface of the component it decorates.
- The Structural Triad:
- Decorator = Adds behaviors (same interface).
- Proxy = Controls access (same interface).
- Adapter = Bridges incompatible APIs (different interface).
- Caveats: Watch out for the “SELF” problem (self-invocation bypass) and object identity inequality (
original != decorator).
Red Flags & Anti-Patterns
- 🚩 Using inheritance for optional cross-cutting features (leading to classes like
FastEncryptedCompressingStream). - 🚩 Confusing Decorator with Adapter: Decorator preserves the interface; Adapter changes it.
- 🚩 Relying on reference equality (
==) between a decorated object and its wrapper. - 🚩 Writing manual Java decorators for logging/caching in Spring applications instead of using Spring AOP (
@Transactional,@Cacheable).
Related Topics
- What Proxy types exist — JDK Dynamic Proxies and CGLIB
- What are design patterns — Composition vs Inheritance fundamentals
- What pattern categories exist — Structural patterns overview
- Difference between State and Strategy — Behavioral pattern dynamics