0
0
LLDsystem_design~7 mins

Decorator pattern in LLD - System Design Guide

Choose your learning style9 modes available
Problem Statement
When you want to add new features to an object without changing its original code, modifying the base class directly can cause bugs and make the system hard to maintain. Also, subclassing for every new feature leads to a large, complex inheritance tree that is difficult to manage and extend.
Solution
The Decorator pattern wraps the original object inside another object that adds new behavior before or after delegating calls to the original. This way, you can layer multiple decorators dynamically without changing the original object's code or creating many subclasses.
Architecture
Client
Decorator
Concrete
Concrete

This diagram shows the client calling a decorator object, which wraps the original component. The decorator can add behavior before or after forwarding calls to the original component.

Trade-offs
✓ Pros
Allows adding responsibilities to objects at runtime without modifying their code.
Supports layering multiple decorators to combine behaviors flexibly.
Keeps classes simple and focused by separating concerns into decorators.
Avoids explosion of subclasses compared to inheritance-based extensions.
✗ Cons
Can result in many small decorator classes, increasing complexity.
Debugging can be harder because behavior is spread across layers.
Performance overhead due to multiple wrapper calls in deep decorator chains.
Use when you need to add features to objects dynamically and want to avoid subclass explosion, especially when behaviors can be combined in different ways.
Avoid when the system is simple with few variations, or when performance is critical and the overhead of multiple wrappers is unacceptable.
Real World Examples
Python standard library
The I/O library uses decorators to add buffering, encoding, and compression to file streams without changing the base stream classes.
Java I/O API
Java uses decorators like BufferedInputStream and DataInputStream to add features to basic InputStream objects dynamically.
Flask web framework
Flask uses decorators to add routes and middleware behaviors to functions without modifying the original function code.
Code Example
The before code uses subclassing to add milk and sugar costs, leading to many subclasses for combinations. The after code wraps the base Coffee object with decorators that add cost dynamically, allowing flexible feature combinations without subclass explosion.
LLD
### Before (without decorator pattern):
class Coffee:
    def cost(self):
        return 5

class MilkCoffee(Coffee):
    def cost(self):
        return super().cost() + 2

class SugarMilkCoffee(MilkCoffee):
    def cost(self):
        return super().cost() + 1

coffee = SugarMilkCoffee()
print(coffee.cost())  # Output: 8


### After (with decorator pattern):
class Coffee:
    def cost(self):
        return 5

class CoffeeDecorator:
    def __init__(self, coffee):
        self._coffee = coffee
    def cost(self):
        return self._coffee.cost()

class MilkDecorator(CoffeeDecorator):
    def cost(self):
        return self._coffee.cost() + 2

class SugarDecorator(CoffeeDecorator):
    def cost(self):
        return self._coffee.cost() + 1

coffee = Coffee()
coffee_with_milk = MilkDecorator(coffee)
coffee_with_milk_and_sugar = SugarDecorator(coffee_with_milk)
print(coffee_with_milk_and_sugar.cost())  # Output: 8
OutputSuccess
Alternatives
Subclassing
Creates new classes for each feature combination at compile time, leading to many subclasses.
Use when: When features are fixed and few, and runtime flexibility is not needed.
Proxy pattern
Focuses on controlling access to an object rather than adding behavior.
Use when: When you need to control or manage access rather than extend functionality.
Aspect-Oriented Programming (AOP)
Injects cross-cutting concerns like logging or security across many classes transparently.
Use when: When you want to apply behaviors across many unrelated classes without manual wrapping.
Summary
Decorator pattern adds new behavior to objects dynamically by wrapping them.
It avoids subclass explosion and keeps code flexible and maintainable.
It is widely used in I/O libraries and frameworks to extend functionality.