Question 12 · Section 2

Advantage of Decorator over Inheritance?

4. Same interface for decorator and component 5. AOP can replace simple decorators

Language versions: English Russian Ukrainian

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

  1. Decorator when runtime composition is needed
  2. Inheritance for “is-a” relationships
  3. Composition preferred over inheritance
  4. Same interface for decorator and component
  5. 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