Understanding the Decorator Pattern: Dynamic Runtime Enhancements with Java Examples
The article explains the Decorator pattern, showing how it dynamically adds functionality to objects without modifying them or using inheritance, using an order pricing example and Java IO streams, compares it with inheritance and Proxy, and outlines when to apply decorators effectively.
Problem: Class Explosion with Inheritance
When an order price must be calculated with independent, combinable discounts—member discount (9 % off), full‑reduction (‑20 when price ≥ 100), and coupon (‑10)—each possible combination requires a separate subclass if inheritance is used. For three discounts this yields seven subclasses; adding a fourth doubles the count (2ⁿ‑1). The inheritance hierarchy is static and cannot express arbitrary runtime combinations.
class Order { double getPrice() { return 100; } }
class MemberOrder extends Order { /* only member discount */ }
class FullReductionOrder extends Order { /* only full reduction */ }
class CouponOrder extends Order { /* only coupon */ }
class MemberFullReductionOrder extends Order { /* member + full reduction */ }
class MemberCouponOrder extends Order { /* member + coupon */ }
class FullReductionCouponOrder extends Order { /* full reduction + coupon */ }
class MemberFullReductionCouponOrder extends Order { /* all three */ }
// 3 discounts already produce 7 subclassesDecorator Pattern: Onion‑style Wrapping
Define a common Order interface and a basic implementation:
public interface Order {
double getPrice(); // calculate price
}
public class BasicOrder implements Order {
private final double price;
public BasicOrder(double price) { this.price = price; }
public double getPrice() { return price; }
}An abstract decorator also implements Order and holds a reference to another Order:
public abstract class OrderDecorator implements Order {
protected final Order order; // the wrapped object
public OrderDecorator(Order order) { this.order = order; }
}Concrete decorators add a single enhancement:
public class MemberDecorator extends OrderDecorator {
public MemberDecorator(Order order) { super(order); }
public double getPrice() { return order.getPrice() * 0.9; }
}
public class FullReductionDecorator extends OrderDecorator {
public FullReductionDecorator(Order order) { super(order); }
public double getPrice() {
double p = order.getPrice();
return p >= 100 ? p - 20 : p;
}
}
public class CouponDecorator extends OrderDecorator {
public CouponDecorator(Order order) { super(order); }
public double getPrice() { return order.getPrice() - 10; }
}Usage demonstrates dynamic stacking:
Order order = new BasicOrder(100); // base price 100
order = new MemberDecorator(order); // 90
order = new FullReductionDecorator(order); // still 90 (price < 100)
order = new CouponDecorator(order); // 80
System.out.println(order.getPrice()); // prints 80Only three decorator classes are needed regardless of how many discounts are combined, and the combination is decided at runtime.
Four Roles of the Pattern
Component : Order interface – defines the common contract.
ConcreteComponent : BasicOrder – the core object being decorated.
Decorator : OrderDecorator – implements Order and holds a reference to a Order.
ConcreteDecorator : MemberDecorator, FullReductionDecorator, CouponDecorator – each adds a specific piece of behavior.
Why Decorator Demonstrates “Composition over Inheritance”
Linear class count : n enhancements require n decorator classes, not 2ⁿ subclasses.
Runtime composition : any combination and order can be assembled in code at runtime.
Open/Closed Principle : adding a new enhancement means adding a new decorator; existing code stays unchanged.
Single responsibility : each decorator handles one concern, keeping logic isolated.
The abstract decorator uses inheritance only to reuse the field that holds the wrapped component; the actual extension behavior relies on composition (the order reference).
Java IO: Classic Real‑World Decorator
Stacking streams in the standard library follows the same pattern:
InputStream in = new BufferedInputStream(
new GZIPInputStream(
new FileInputStream("data.gz")));Component : InputStream – common abstract stream.
ConcreteComponent : FileInputStream – reads raw bytes from a file.
AbstractDecorator : FilterInputStream – holds an InputStream.
ConcreteDecorators : BufferedInputStream (adds buffering), GZIPInputStream (adds compression), DataInputStream (adds primitive‑type reading), etc.
Without decorators, each combination of source and processing capability would require a separate subclass (e.g., BufferedFileInputStream, GzipBufferedFileInputStream), leading to another 2ⁿ explosion.
Decorator vs Proxy: Intentual Distinction
Intent : Proxy controls access; Decorator enhances functionality.
Object handling : Proxy creates or manages the real object internally; Decorator receives the real object from outside.
Typical layers : Proxy usually a single layer; Decorator often multiple layers for stacking.
Focus : Proxy concerns operations on the real object (logging, security, remote invocation); Decorator concerns augmenting the object's own capabilities (buffering, discounting).
Typical scenarios : Proxy – Spring AOP, RPC, permission checks; Decorator – Java IO streams, discount stacking.
The decisive test is to look at who supplies the inner object. If the client creates it with new and passes it to the wrapper, it is a decorator; if the wrapper hides the real object, it is a proxy.
When to Use Decorator
Signals to apply :
Dynamic, optional, combinable features are needed, and the features can be freely combined.
Using inheritance would cause class explosion because of multiple independent dimensions.
The enhancement logic must be flexible at runtime rather than fixed at compile time.
Signals to avoid :
The set of features is fixed or only one enhancement is required – inheritance or a simple method may be simpler.
The goal is access control rather than capability extension – a proxy is appropriate.
Only a single enhancement is needed and stacking is unnecessary.
Overhead: each decorator adds a small object and deepens the call stack, which can make debugging more involved.
Conclusion
Decorator lets you wrap an object layer by layer, adding behavior without modifying the original class or relying on inheritance. The pattern’s dual nature—being both a component and holding a component—realizes composition over inheritance, eliminates exponential class growth, and is exemplified by Java IO’s stackable streams. The key distinction from proxy is intent: you actively stack decorators to enhance; a proxy sits in front to control access.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
