Advantage of Decorator over Inheritance?
4. Same interface for decorator and component 5. AOP can replace simple decorators
Junior Level
Decorator is a pattern that adds new functionality to an object by wrapping it in another object.
Inheritance problem: If you need different combinations of features, you get a class explosion.
Inheritance example:
// 2^n classes for n functions
class Coffee { }
class MilkCoffee extends Coffee { }
class SugarCoffee extends Coffee { }
class MilkSugarCoffee extends Coffee { } // Already 4 classes!
class WhippedCreamMilkSugarCoffee extends Coffee { } // And more...
Solution — Decorator:
// n classes for n functions
interface Coffee { double getCost(); }
class SimpleCoffee implements Coffee {
public double getCost() { return 1.0; }
}
// Decorators
class MilkDecorator implements Coffee {
private final Coffee coffee;
public MilkDecorator(Coffee coffee) { this.coffee = coffee; }
public double getCost() { return coffee.getCost() + 0.5; }
}
class SugarDecorator implements Coffee {
private final Coffee coffee;
public SugarDecorator(Coffee coffee) { this.coffee = coffee; }
public double getCost() { return coffee.getCost() + 0.2; }
}
// Any combination!
Coffee order = new MilkDecorator(
new SugarDecorator(
new SimpleCoffee()
)
);
System.out.println(order.getCost()); // 1.7
Key advantage: Decorators can be combined arbitrarily without creating new classes.
Middle Level
Comparison: Inheritance vs Decorator
| Criterion | Inheritance | Decorator |
|---|---|---|
| When added | Compile-time | Runtime |
| Class count | 2^n | n decorators + base component |
| Combinations | Fixed | Any |
| Flexibility | Low | High |
Java I/O — Classic Example
// Decorator in action
InputStream input = new BufferedInputStream(
new CipherInputStream(
new GZIPInputStream(
new FileInputStream("file.txt")
)
)
);
// Each layer is a decorator:
// FileInputStream -> reads file
// GZIPInputStream -> decompresses data
// CipherInputStream -> decrypts
// BufferedInputStream -> buffers
When NOT to use Decorator
// Overengineering for simple functionality
class LoggingUserService extends UserService {
@Override
public void create(User user) {
log.info("Creating user: " + user);
super.create(user);
}
}
// Composition or AOP is sufficient
Senior Level
Composition over Inheritance
Decorator = special case of composition
Inheritance: "is-a" (Coffee IS-A Beverage)
Composition: "has-a" (Decorator HAS-A Coffee)
-> Composition is more flexible and less fragile
Dynamic Responsibility
// This is Chain of Responsibility / Pipeline, not classic Decorator.
// Classic Decorator = nested wrappers: new Compression(new Encryption(new Stream()))
// Adding/removing responsibilities at runtime
public class Order {
private List<OrderDecorator> decorators = new ArrayList<>();
private BigDecimal basePrice;
public void addDecorator(OrderDecorator d) { decorators.add(d); }
public void removeDecorator(Class<? extends OrderDecorator> type) {
decorators.removeIf(d -> d.getClass().equals(type));
}
public BigDecimal getPrice() {
BigDecimal price = basePrice;
for (OrderDecorator d : decorators) {
price = d.calculate(price);
}
return price;
}
}
Production Experience
Real scenario: Java I/O Decorator
- Spring: ResponseBodyAdvice = Decorator for HTTP responses
- Adds logging, compression, encryption
- Without Decorator: would need to inherit controllers
Best Practices
- Decorator when runtime composition is needed
- Inheritance for “is-a” relationships
- Composition preferred over inheritance
- Same interface for decorator and component
- AOP can replace simple decorators
Senior Summary
- Decorator = runtime addition of responsibilities
- Composition > Inheritance — more flexible and less fragile
- Java I/O = classic Decorator example
- 2^n vs n — scalability difference
- AOP replaces simple decorators (@Transactional)
Interview Cheat Sheet
Must know:
- Decorator adds functionality via wrapping, inheritance — via class extension
- Scalability: Decorator = n classes for n functions, inheritance = 2^n classes (combinatorial explosion)
- Java I/O — classic example: BufferedInputStream(CipherInputStream(GZIPInputStream(FileInputStream)))
- Composition over Inheritance: Decorator is a special case of composition, less fragile than inheritance
- Decorator works at runtime, inheritance — at compile-time
- AOP (@Transactional, @Cacheable) replaces simple decorators
- Dynamic addition/removal of decorators at runtime (Pipeline/Chain pattern)
Common follow-up questions:
- When is Decorator overengineering? — Simple functionality, composition or AOP is enough
- How does Decorator differ from Adapter? — Decorator adds functionality, Adapter changes interface
- Why is Java I/O a Decorator example? — Each stream wraps another, adding its own function
- Can decorators be removed at runtime? — Yes, if you store a list of decorators (Pipeline pattern)
Red flags (DO NOT say):
- “Inheritance is always better than Decorator” — combinatorial class explosion (2^n)
- “Decorator and Proxy are the same” — Proxy controls access, Decorator adds functionality
- “I use Decorator for logging” — AOP (@Aspect) is more suitable
- “Decorator is hard to understand” — Java I/O uses it, it’s intuitive
Related topics:
- [[14. What Proxy types exist]] — Proxy vs Decorator
- [[01. What are design patterns]] — composition vs inheritance
- [[02. What pattern categories exist]] — Structural patterns
- [[16. What anti-patterns do you know]] — God Object, when Decorator is not used
- [[13. Difference between State and Strategy]] — behavioral patterns