Why Inheritance Fails: Mastering the Decorator Pattern in Java
This article explains the Decorator design pattern, contrasting it with inheritance, and shows how to dynamically extend Java classes—especially I/O streams—using composition, complete with code examples, class diagrams, advantages, drawbacks, and practical usage scenarios.
1. Introduction
The article starts by stating that there are two ways to add behavior to a class or object: inheritance (static) and association (dynamic via a decorator).
2. Definition
Decorator is a structural pattern that dynamically adds responsibilities to an object without altering its interface, keeping the added functionality transparent to the client.
3. Architecture
Component : abstract interface for objects that can be decorated.
ConcreteComponent : concrete class implementing the component.
Decorator : abstract class extending the component and holding a reference to a component.
ConcreteDecorator : concrete decorator adding new responsibilities.
4. Java I/O Example
Java I/O provides many classes (FileInputStream, BufferedInputStream, DataInputStream, etc.). To read a file with buffering, the usual code is:
InputStream in = new FileInputStream("/user/javaedge/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
// ...
}This approach requires creating a raw stream and then wrapping it, which feels cumbersome. A hypothetical BufferedFileInputStream would simplify the code:
InputStream bin = new BufferedFileInputStream("/user/javaedge/test.txt");
byte[] data = new byte[128];
while (bin.read(data) != -1) {
// ...
}However, extending every InputStream subclass with a buffered version would explode the class hierarchy.
4.1 Inheritance Design Issues
Creating a buffered subclass for each concrete stream (e.g., BufferedFileInputStream, BufferedPipedInputStream) leads to a combinatorial explosion when additional features like DataInputStream are needed.
4.2 Decorator Design Solution
By using composition, a BufferedInputStream can wrap any InputStream and add buffering without altering the original class. The same applies to DataInputStream for primitive‑type reads.
public abstract class InputStream {
public int read(byte[] b) throws IOException { return read(b, 0, b.length); }
public abstract int read(byte[] b, int off, int len) throws IOException;
// other methods omitted for brevity
}
public class BufferedInputStream extends InputStream {
protected volatile InputStream in;
protected BufferedInputStream(InputStream in) { this.in = in; }
@Override public int read(byte[] b, int off, int len) throws IOException { return in.read(b, off, len); }
// additional buffering logic here
}
public class DataInputStream extends InputStream {
protected volatile InputStream in;
protected DataInputStream(InputStream in) { this.in = in; }
public int readInt() throws IOException { /* read 4 bytes and convert */ }
// other primitive read methods
}Both decorators inherit from InputStream but delegate actual work to the wrapped stream, avoiding code duplication.
4.3 FilterInputStream – Reducing Duplication
Java introduces FilterInputStream as a common abstract decorator. All concrete decorators (e.g., BufferedInputStream, DataInputStream) extend it, inheriting default delegations for methods like read(), skip(), close(), etc.
public class FilterInputStream extends InputStream {
protected volatile InputStream in;
protected FilterInputStream(InputStream in) { this.in = in; }
@Override public int read() throws IOException { return in.read(); }
@Override public int read(byte[] b) throws IOException { return in.read(b); }
@Override public long skip(long n) throws IOException { return in.skip(n); }
@Override public void close() throws IOException { in.close(); }
// other delegations omitted
}5. Comparison with Proxy Pattern
Both Proxy and Decorator add behavior, but Proxy usually adds unrelated responsibilities (e.g., remote access, security) and hides the underlying object, whereas Decorator adds responsibilities that are conceptually part of the original object and keeps the interface identical.
6. Advantages
Dynamic, transparent addition of responsibilities.
Better low‑coupling compared to inheritance.
Supports multiple independent extensions by stacking decorators.
Adheres to the Open/Closed Principle.
7. Disadvantages
Creates many small objects, increasing system complexity.
Debugging can be harder because behavior is spread across several decorators.
8. When to Use
Use Decorator when you need to add responsibilities to individual objects at runtime without affecting other objects, especially when inheritance would lead to an explosion of subclasses or when the class is final.
9. Variants
Transparent Decorator : client works only with the abstract component type.
Half‑Transparent Decorator : client can reference concrete decorators to use added methods.
10. Summary
The Decorator pattern solves the problem of rigid inheritance by replacing it with composition, allowing flexible, runtime‑configurable extensions while keeping the original interface unchanged.
Reference: https://zh.wikipedia.org/wiki/装饰模式
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.
JavaEdge
First‑line development experience at multiple leading tech firms; now a software architect at a Shanghai state‑owned enterprise and founder of Programming Yanxuan. Nearly 300k followers online; expertise in distributed system design, AIGC application development, and quantitative finance investing.
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.
