84. Decorator Pattern
The Decorator pattern wraps an object with additional behaviour by implementing the same interface and delegating to the wrapped object โ the java.io stream hierarchy is the canonical Java example.
Explanation
Decorator adds responsibilities to an object dynamically without subclassing. It implements the same interface as the object it wraps (ensuring transparency to clients) and forwards calls to the inner object, optionally adding behaviour before or after.
The java.io stream classes are built entirely on Decorator: InputStream is the base; FileInputStream provides bytes from a file; BufferedInputStream(new FileInputStream(...)) wraps it with buffering; DataInputStream(new BufferedInputStream(...)) adds typed reads. Each decorator adds one layer of behaviour; they compose arbitrarily.
Modern alternative in many cases: default methods and composition in Java 8+ โ but the classic Decorator structure (wrap-and-delegate) remains the clearest solution for cross-cutting behaviour like logging, caching, access control, and buffering.
Example
import java.util.function.Function;
public class DecoratorPattern {
// Component interface
interface DataService {
String fetchData(String key);
}
// Concrete component
static class DatabaseService implements DataService {
@Override
public String fetchData(String key) {
// Simulate slow DB call
try { Thread.sleep(10); } catch (InterruptedException e) {}
return "data-for-" + key;
}
}
// Decorator: adds logging
static class LoggingDecorator implements DataService {
private final DataService delegate;
LoggingDecorator(DataService delegate) { this.delegate = delegate; }
@Override
public String fetchData(String key) {
System.out.println("[LOG] Fetching: " + key);
String result = delegate.fetchData(key);
System.out.println("[LOG] Got: " + result);
return result;
}
}
// Decorator: adds caching
static class CachingDecorator implements DataService {
private final DataService delegate;
private final java.util.Map<String, String> cache = new java.util.HashMap<>();
CachingDecorator(DataService delegate) { this.delegate = delegate; }
@Override
public String fetchData(String key) {
return cache.computeIfAbsent(key, delegate::fetchData);
}
}
public static void main(String[] args) {
// Compose: DB + Cache + Logging
DataService service = new LoggingDecorator(
new CachingDecorator(
new DatabaseService()));
System.out.println(service.fetchData("user:1")); // fetches + logs
System.out.println(service.fetchData("user:1")); // from cache (still logged)
System.out.println(service.fetchData("user:2")); // new fetch
// Java IO: classic Decorator usage
System.out.println("\nJava IO Decorator chain:");
try (var in = new java.io.DataInputStream(
new java.io.BufferedInputStream(
new java.io.FileInputStream(java.io.FileDescriptor.in)))) {
System.out.println("Layered stream: DataInputStream -> BufferedInputStream -> FileInputStream");
// Each layer adds one behaviour without modifying the others
} catch (Exception e) { /* ignore for demo */ }
}
}Sources
- https://refactoring.guru/design-patterns/decorator โ Decorator pattern reference
- https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/FilterInputStream.html โ FilterInputStream (Decorator base)