Bird
Raised Fist0
Interview Prepoop-design-patternsmediumAmazonGoogleFlipkartSwiggyCRED

Decorator Pattern - Wrapping Behaviour Without Subclassing

Choose your preparation mode3 modes available
🎯
Decorator Pattern - Wrapping Behaviour Without Subclassing
mediumOOPAmazonGoogleFlipkart

Imagine you want to add new features to a coffee order, like milk or sugar, without creating a new subclass for every combination. How can you dynamically add these behaviors at runtime?

💡 This problem is about adding new behavior to objects without changing their class or creating many subclasses. Beginners often struggle because they try to use inheritance for every variation, which leads to class explosion and rigid designs. The decorator pattern solves this by wrapping objects to add features dynamically.
📋
Problem Statement

Design a system where you can add responsibilities to objects dynamically without subclassing. Implement the Decorator pattern to wrap an object and extend its behavior at runtime. The system should allow stacking multiple decorators transparently.

The base component and decorators must share a common interface.Decorators should be able to wrap both concrete components and other decorators.The design should avoid subclass explosion and allow flexible behavior composition.
💡
Example
Input"Create a Coffee object, then wrap it with MilkDecorator and SugarDecorator."
OutputThe final cost and description reflect coffee with milk and sugar added.

The Coffee object is wrapped first by MilkDecorator which adds milk cost and description, then by SugarDecorator which adds sugar cost and description. The calls delegate through the wrappers, accumulating behavior.

  • Decorating an object with no decorators → returns base behavior
  • Multiple decorators wrapping each other → combined behavior
  • Decorator wrapping null or invalid component → should handle gracefully
  • Decorator that adds no behavior → should not alter output
⚠️
Common Mistakes
Trying to add behavior by modifying the base class directly

Leads to rigid code and violates open-closed principle

✅ Use decorators to add behavior without changing existing classes

Not delegating calls properly in decorator methods

Behavior is lost or incomplete, causing incorrect results

✅ Always delegate to the wrapped component before or after adding behavior

Creating decorators that do not implement the component interface

Breaks polymorphism and causes runtime errors

✅ Ensure decorators implement the same interface as components

Stacking decorators incorrectly or in wrong order

Unexpected behavior or incorrect results

✅ Carefully order decorators to reflect desired behavior composition

Using inheritance instead of composition for decorators

Leads to subclass explosion and inflexible design

✅ Use composition to wrap components dynamically

🧠
Brute Force (Subclassing for Every Variation)
💡 This approach shows the naive way beginners often try: creating a subclass for every combination of features. It helps understand why this is impractical and motivates the decorator pattern.

Intuition

Create a subclass for each combination of added behaviors, e.g., CoffeeWithMilk, CoffeeWithSugar, CoffeeWithMilkAndSugar. Each subclass overrides methods to add behavior.

Algorithm

  1. Define a base component class with core behavior.
  2. Create subclasses for each added feature, overriding methods to extend behavior.
  3. For combinations, create subclasses inheriting from other subclasses to combine features.
  4. Use instances of these subclasses to get desired behavior.
💡 This approach is straightforward but quickly becomes complex and hard to maintain as features combine.
</>
Code
class Coffee:
    def cost(self):
        return 5
    def description(self):
        return "Coffee"

class CoffeeWithMilk(Coffee):
    def cost(self):
        return super().cost() + 1
    def description(self):
        return super().description() + ", Milk"

class CoffeeWithSugar(Coffee):
    def cost(self):
        return super().cost() + 0.5
    def description(self):
        return super().description() + ", Sugar"

class CoffeeWithMilkAndSugar(CoffeeWithMilk):
    def cost(self):
        return super().cost() + 0.5
    def description(self):
        return super().description() + ", Sugar"

# Driver code
if __name__ == "__main__":
    c = CoffeeWithMilkAndSugar()
    print(c.description())  # Coffee, Milk, Sugar
    print(c.cost())         # 6.5
Line Notes
class Coffee:Defines the base component with core behavior to represent plain coffee
def cost(self):Returns the base cost of coffee without any additions
class CoffeeWithMilk(Coffee):Subclass that adds milk behavior by overriding cost and description
return super().cost() + 1Adds the cost of milk to the base coffee cost
class Coffee {
    public double cost() {
        return 5;
    }
    public String description() {
        return "Coffee";
    }
}

class CoffeeWithMilk extends Coffee {
    @Override
    public double cost() {
        return super.cost() + 1;
    }
    @Override
    public String description() {
        return super.description() + ", Milk";
    }
}

class CoffeeWithSugar extends Coffee {
    @Override
    public double cost() {
        return super.cost() + 0.5;
    }
    @Override
    public String description() {
        return super.description() + ", Sugar";
    }
}

class CoffeeWithMilkAndSugar extends CoffeeWithMilk {
    @Override
    public double cost() {
        return super.cost() + 0.5;
    }
    @Override
    public String description() {
        return super.description() + ", Sugar";
    }
}

public class Main {
    public static void main(String[] args) {
        Coffee c = new CoffeeWithMilkAndSugar();
        System.out.println(c.description()); // Coffee, Milk, Sugar
        System.out.println(c.cost());        // 6.5
    }
}
Line Notes
class Coffee {Base component class with core methods representing plain coffee
public double cost() {Returns the base cost of coffee
class CoffeeWithMilk extends Coffee {Subclass that adds milk behavior by overriding methods
return super.cost() + 1;Adds milk cost to the base coffee cost
#include <iostream>
#include <string>

class Coffee {
public:
    virtual double cost() { return 5; }
    virtual std::string description() { return "Coffee"; }
    virtual ~Coffee() {}
};

class CoffeeWithMilk : public Coffee {
public:
    double cost() override { return Coffee::cost() + 1; }
    std::string description() override { return Coffee::description() + ", Milk"; }
};

class CoffeeWithSugar : public Coffee {
public:
    double cost() override { return Coffee::cost() + 0.5; }
    std::string description() override { return Coffee::description() + ", Sugar"; }
};

class CoffeeWithMilkAndSugar : public CoffeeWithMilk {
public:
    double cost() override { return CoffeeWithMilk::cost() + 0.5; }
    std::string description() override { return CoffeeWithMilk::description() + ", Sugar"; }
};

int main() {
    Coffee* c = new CoffeeWithMilkAndSugar();
    std::cout << c->description() << std::endl; // Coffee, Milk, Sugar
    std::cout << c->cost() << std::endl;        // 6.5
    delete c;
    return 0;
}
Line Notes
class Coffee {Base component with virtual methods for polymorphism representing plain coffee
virtual double cost() { return 5; }Returns base cost of coffee
class CoffeeWithMilk : public Coffee {Subclass that adds milk behavior by overriding methods
double cost() override { return Coffee::cost() + 1; }Adds milk cost to base coffee cost
class Coffee {
    cost() {
        return 5;
    }
    description() {
        return "Coffee";
    }
}

class CoffeeWithMilk extends Coffee {
    cost() {
        return super.cost() + 1;
    }
    description() {
        return super.description() + ", Milk";
    }
}

class CoffeeWithSugar extends Coffee {
    cost() {
        return super.cost() + 0.5;
    }
    description() {
        return super.description() + ", Sugar";
    }
}

class CoffeeWithMilkAndSugar extends CoffeeWithMilk {
    cost() {
        return super.cost() + 0.5;
    }
    description() {
        return super.description() + ", Sugar";
    }
}

// Driver code
const c = new CoffeeWithMilkAndSugar();
console.log(c.description()); // Coffee, Milk, Sugar
console.log(c.cost());        // 6.5
Line Notes
class Coffee {Defines base component class representing plain coffee
cost() {Returns base cost of coffee
class CoffeeWithMilk extends Coffee {Subclass that adds milk behavior by overriding methods
return super.cost() + 1;Adds milk cost to base coffee cost
⏱
Complexity
TimeO(1) per call
SpaceO(k) for k subclasses created

Each subclass adds fixed overhead, but the number of subclasses grows exponentially with feature combinations.

💡 For 3 features, you might need up to 8 subclasses, which is unmanageable for many features.
Interview Verdict: Accepted but impractical

This approach works but is not scalable or maintainable, motivating the decorator pattern.

🧠
Decorator Pattern Using Composition
💡 This approach introduces the decorator pattern, which solves the subclass explosion by wrapping objects dynamically. It teaches composition over inheritance and flexible behavior extension.

Intuition

Instead of subclassing, create decorator classes that hold a reference to a component and add behavior before or after delegating calls to it.

Algorithm

  1. Define a component interface with core methods.
  2. Implement a concrete component class.
  3. Create decorator classes implementing the same interface and holding a reference to a component.
  4. In decorator methods, add behavior and delegate calls to the wrapped component.
💡 The key is delegation and wrapping, which may be tricky to grasp initially but enables flexible behavior stacking.
</>
Code
from abc import ABC, abstractmethod

class Coffee(ABC):
    @abstractmethod
    def cost(self):
        pass
    @abstractmethod
    def description(self):
        pass

class SimpleCoffee(Coffee):
    def cost(self):
        return 5
    def description(self):
        return "Coffee"

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

class MilkDecorator(CoffeeDecorator):
    def cost(self):
        return super().cost() + 1
    def description(self):
        return super().description() + ", Milk"

class SugarDecorator(CoffeeDecorator):
    def cost(self):
        return super().cost() + 0.5
    def description(self):
        return super().description() + ", Sugar"

# Driver code
if __name__ == "__main__":
    coffee = SimpleCoffee()
    coffee = MilkDecorator(coffee)
    coffee = SugarDecorator(coffee)
    print(coffee.description())  # Coffee, Milk, Sugar
    print(coffee.cost())         # 6.5
Line Notes
class Coffee(ABC):Defines the component interface abstractly to enforce method implementation
def __init__(self, coffee):Decorator holds reference to wrapped component for delegation
def cost(self):Decorator delegates cost call to the wrapped component
return super().cost() + 1MilkDecorator adds milk cost on top of wrapped component's cost
interface Coffee {
    double cost();
    String description();
}

class SimpleCoffee implements Coffee {
    public double cost() { return 5; }
    public String description() { return "Coffee"; }
}

abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;
    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }
    public double cost() {
        return coffee.cost();
    }
    public String description() {
        return coffee.description();
    }
}

class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }
    public double cost() {
        return super.cost() + 1;
    }
    public String description() {
        return super.description() + ", Milk";
    }
}

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }
    public double cost() {
        return super.cost() + 0.5;
    }
    public String description() {
        return super.description() + ", Sugar";
    }
}

public class Main {
    public static void main(String[] args) {
        Coffee coffee = new SimpleCoffee();
        coffee = new MilkDecorator(coffee);
        coffee = new SugarDecorator(coffee);
        System.out.println(coffee.description()); // Coffee, Milk, Sugar
        System.out.println(coffee.cost());        // 6.5
    }
}
Line Notes
interface Coffee {Defines the component interface to ensure consistent methods
protected Coffee coffee;Decorator holds reference to wrapped component for delegation
public double cost() {Decorator delegates cost call to wrapped component
return super.cost() + 1;MilkDecorator adds milk cost on top of wrapped component
#include <iostream>
#include <string>
#include <memory>

class Coffee {
public:
    virtual double cost() = 0;
    virtual std::string description() = 0;
    virtual ~Coffee() {}
};

class SimpleCoffee : public Coffee {
public:
    double cost() override { return 5; }
    std::string description() override { return "Coffee"; }
};

class CoffeeDecorator : public Coffee {
protected:
    std::shared_ptr<Coffee> coffee;
public:
    CoffeeDecorator(std::shared_ptr<Coffee> c) : coffee(c) {}
    double cost() override { return coffee->cost(); }
    std::string description() override { return coffee->description(); }
};

class MilkDecorator : public CoffeeDecorator {
public:
    MilkDecorator(std::shared_ptr<Coffee> c) : CoffeeDecorator(c) {}
    double cost() override { return CoffeeDecorator::cost() + 1; }
    std::string description() override { return CoffeeDecorator::description() + ", Milk"; }
};

class SugarDecorator : public CoffeeDecorator {
public:
    SugarDecorator(std::shared_ptr<Coffee> c) : CoffeeDecorator(c) {}
    double cost() override { return CoffeeDecorator::cost() + 0.5; }
    std::string description() override { return CoffeeDecorator::description() + ", Sugar"; }
};

int main() {
    std::shared_ptr<Coffee> coffee = std::make_shared<SimpleCoffee>();
    coffee = std::make_shared<MilkDecorator>(coffee);
    coffee = std::make_shared<SugarDecorator>(coffee);
    std::cout << coffee->description() << std::endl; // Coffee, Milk, Sugar
    std::cout << coffee->cost() << std::endl;        // 6.5
    return 0;
}
Line Notes
class Coffee {Abstract component interface defining required methods
std::shared_ptr<Coffee> coffee;Decorator holds shared pointer to wrapped component for safe memory management
double cost() override { return coffee->cost(); }Delegates cost call to wrapped component
double cost() override { return CoffeeDecorator::cost() + 1; }MilkDecorator adds milk cost on top of wrapped component
class Coffee {
    cost() {
        throw new Error("Method not implemented");
    }
    description() {
        throw new Error("Method not implemented");
    }
}

class SimpleCoffee extends Coffee {
    cost() {
        return 5;
    }
    description() {
        return "Coffee";
    }
}

class CoffeeDecorator extends Coffee {
    constructor(coffee) {
        super();
        this.coffee = coffee;
    }
    cost() {
        return this.coffee.cost();
    }
    description() {
        return this.coffee.description();
    }
}

class MilkDecorator extends CoffeeDecorator {
    cost() {
        return super.cost() + 1;
    }
    description() {
        return super.description() + ", Milk";
    }
}

class SugarDecorator extends CoffeeDecorator {
    cost() {
        return super.cost() + 0.5;
    }
    description() {
        return super.description() + ", Sugar";
    }
}

// Driver code
const coffee = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
console.log(coffee.description()); // Coffee, Milk, Sugar
console.log(coffee.cost());        // 6.5
Line Notes
class Coffee {Defines component interface with unimplemented methods to enforce implementation
constructor(coffee) {Decorator stores reference to wrapped component for delegation
cost() { return this.coffee.cost(); }Delegates cost call to wrapped component
return super.cost() + 1;MilkDecorator adds milk cost on top of wrapped component
⏱
Complexity
TimeO(k) for k decorators stacked
SpaceO(k) for k decorator objects

Each decorator adds a small overhead and delegates calls, so cost and description calls traverse the decorator chain.

💡 For 3 decorators, 3 method calls are chained, which is efficient and scalable.
Interview Verdict: Accepted and recommended

This is the canonical solution for flexible behavior extension without subclass explosion.

🧠
Decorator Pattern with Dynamic Behavior Injection
💡 This approach extends the decorator pattern by allowing decorators to accept parameters or functions to customize behavior dynamically, increasing flexibility.

Intuition

Decorators can be parameterized or accept lambdas to add behavior dynamically, e.g., adding variable amounts of milk or sugar or logging dynamically.

Algorithm

  1. Define component interface and concrete component as before.
  2. Create decorators that accept parameters or functions in their constructor.
  3. In decorator methods, use these parameters to modify behavior dynamically.
  4. Stack decorators as needed to compose complex behavior.
💡 This approach shows how decorators can be more than static wrappers, enabling runtime customization.
</>
Code
from abc import ABC, abstractmethod

class Coffee(ABC):
    @abstractmethod
    def cost(self):
        pass
    @abstractmethod
    def description(self):
        pass

class SimpleCoffee(Coffee):
    def cost(self):
        return 5
    def description(self):
        return "Coffee"

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

class MilkDecorator(CoffeeDecorator):
    def __init__(self, coffee, amount=1):
        super().__init__(coffee)
        self.amount = amount
    def cost(self):
        return super().cost() + 0.5 * self.amount
    def description(self):
        return super().description() + f", Milk({self.amount})"

class SugarDecorator(CoffeeDecorator):
    def __init__(self, coffee, amount=1):
        super().__init__(coffee)
        self.amount = amount
    def cost(self):
        return super().cost() + 0.2 * self.amount
    def description(self):
        return super().description() + f", Sugar({self.amount})"

# Driver code
if __name__ == "__main__":
    coffee = SimpleCoffee()
    coffee = MilkDecorator(coffee, amount=2)
    coffee = SugarDecorator(coffee, amount=3)
    print(coffee.description())  # Coffee, Milk(2), Sugar(3)
    print(coffee.cost())         # 5 + 1.0 + 0.6 = 6.6
Line Notes
def __init__(self, coffee, amount=1):Decorator accepts parameter to customize behavior dynamically
self.amount = amountStores amount to use in cost and description calculations
return super().cost() + 0.5 * self.amountCost scales with amount of milk added
return super().description() + f", Milk({self.amount})"Description shows amount dynamically for clarity
interface Coffee {
    double cost();
    String description();
}

class SimpleCoffee implements Coffee {
    public double cost() { return 5; }
    public String description() { return "Coffee"; }
}

abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;
    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }
    public double cost() { return coffee.cost(); }
    public String description() { return coffee.description(); }
}

class MilkDecorator extends CoffeeDecorator {
    private int amount;
    public MilkDecorator(Coffee coffee, int amount) {
        super(coffee);
        this.amount = amount;
    }
    public double cost() { return super.cost() + 0.5 * amount; }
    public String description() { return super.description() + ", Milk(" + amount + ")"; }
}

class SugarDecorator extends CoffeeDecorator {
    private int amount;
    public SugarDecorator(Coffee coffee, int amount) {
        super(coffee);
        this.amount = amount;
    }
    public double cost() { return super.cost() + 0.2 * amount; }
    public String description() { return super.description() + ", Sugar(" + amount + ")"; }
}

public class Main {
    public static void main(String[] args) {
        Coffee coffee = new SimpleCoffee();
        coffee = new MilkDecorator(coffee, 2);
        coffee = new SugarDecorator(coffee, 3);
        System.out.println(coffee.description()); // Coffee, Milk(2), Sugar(3)
        System.out.println(coffee.cost());        // 6.6
    }
}
Line Notes
private int amount;Stores amount parameter for dynamic behavior customization
public MilkDecorator(Coffee coffee, int amount) {Constructor accepts amount to customize milk addition
return super.cost() + 0.5 * amount;Cost scales with amount of milk added
return super.description() + ", Milk(" + amount + ")";Description shows amount dynamically for clarity
#include <iostream>
#include <string>
#include <memory>

class Coffee {
public:
    virtual double cost() = 0;
    virtual std::string description() = 0;
    virtual ~Coffee() {}
};

class SimpleCoffee : public Coffee {
public:
    double cost() override { return 5; }
    std::string description() override { return "Coffee"; }
};

class CoffeeDecorator : public Coffee {
protected:
    std::shared_ptr<Coffee> coffee;
public:
    CoffeeDecorator(std::shared_ptr<Coffee> c) : coffee(c) {}
    double cost() override { return coffee->cost(); }
    std::string description() override { return coffee->description(); }
};

class MilkDecorator : public CoffeeDecorator {
    int amount;
public:
    MilkDecorator(std::shared_ptr<Coffee> c, int amt) : CoffeeDecorator(c), amount(amt) {}
    double cost() override { return CoffeeDecorator::cost() + 0.5 * amount; }
    std::string description() override { return CoffeeDecorator::description() + ", Milk(" + std::to_string(amount) + ")"; }
};

class SugarDecorator : public CoffeeDecorator {
    int amount;
public:
    SugarDecorator(std::shared_ptr<Coffee> c, int amt) : CoffeeDecorator(c), amount(amt) {}
    double cost() override { return CoffeeDecorator::cost() + 0.2 * amount; }
    std::string description() override { return CoffeeDecorator::description() + ", Sugar(" + std::to_string(amount) + ")"; }
};

int main() {
    std::shared_ptr<Coffee> coffee = std::make_shared<SimpleCoffee>();
    coffee = std::make_shared<MilkDecorator>(coffee, 2);
    coffee = std::make_shared<SugarDecorator>(coffee, 3);
    std::cout << coffee->description() << std::endl; // Coffee, Milk(2), Sugar(3)
    std::cout << coffee->cost() << std::endl;        // 6.6
    return 0;
}
Line Notes
int amount;Stores amount parameter for dynamic behavior customization
MilkDecorator(std::shared_ptr<Coffee> c, int amt) : CoffeeDecorator(c), amount(amt) {}Constructor accepts amount to customize milk addition
double cost() override { return CoffeeDecorator::cost() + 0.5 * amount; }Cost scales with amount of milk added
std::string description() override { return CoffeeDecorator::description() + ", Milk(" + std::to_string(amount) + ")"; }Description shows amount dynamically for clarity
class Coffee {
    cost() {
        throw new Error("Method not implemented");
    }
    description() {
        throw new Error("Method not implemented");
    }
}

class SimpleCoffee extends Coffee {
    cost() {
        return 5;
    }
    description() {
        return "Coffee";
    }
}

class CoffeeDecorator extends Coffee {
    constructor(coffee) {
        super();
        this.coffee = coffee;
    }
    cost() {
        return this.coffee.cost();
    }
    description() {
        return this.coffee.description();
    }
}

class MilkDecorator extends CoffeeDecorator {
    constructor(coffee, amount = 1) {
        super(coffee);
        this.amount = amount;
    }
    cost() {
        return super.cost() + 0.5 * this.amount;
    }
    description() {
        return super.description() + `, Milk(${this.amount})`;
    }
}

class SugarDecorator extends CoffeeDecorator {
    constructor(coffee, amount = 1) {
        super(coffee);
        this.amount = amount;
    }
    cost() {
        return super.cost() + 0.2 * this.amount;
    }
    description() {
        return super.description() + `, Sugar(${this.amount})`;
    }
}

// Driver code
const coffee = new SugarDecorator(new MilkDecorator(new SimpleCoffee(), 2), 3);
console.log(coffee.description()); // Coffee, Milk(2), Sugar(3)
console.log(coffee.cost());        // 6.6
Line Notes
constructor(coffee, amount = 1) {Decorator accepts amount parameter to customize behavior dynamically
this.amount = amount;Stores amount for use in cost and description calculations
return super.cost() + 0.5 * this.amount;Cost scales with amount of milk added
return super.description() + `, Milk(${this.amount})`;Description shows amount dynamically for clarity
⏱
Complexity
TimeO(k) for k decorators stacked
SpaceO(k) for k decorator objects

Similar to previous approach but with added flexibility from parameters.

💡 Allows dynamic customization without subclassing or code duplication.
Interview Verdict: Accepted and highly flexible

This approach is best for real-world scenarios requiring dynamic behavior customization.

📊
All Approaches - One-Glance Tradeoffs
💡 In interviews, always prefer the decorator pattern using composition (Approach 2 or 3). Approach 1 is useful to mention as a naive baseline but not to code.
ApproachTimeSpaceStack RiskReconstructUse In Interview
1. Brute Force (Subclassing for Every Variation)O(1) per callO(2^k) subclasses for k featuresNoN/AMention only - never code due to scalability issues
2. Decorator Pattern Using CompositionO(k) for k decorators stackedO(k) decorator objectsNoYes, behavior can be composed and inspectedPreferred approach to implement and explain
3. Decorator Pattern with Dynamic Behavior InjectionO(k) for k decorators stackedO(k) decorator objects with parametersNoYes, with added flexibilityUse when dynamic customization is required
💼
Interview Strategy
💡 Use this guide to understand the decorator pattern from naive subclassing to flexible composition. Practice explaining why subclass explosion is bad and how decorators solve it. Code the decorator pattern fluently in your preferred language before interviews.

How to Present

Step 1: Clarify the problem and confirm the need for dynamic behavior extension.Step 2: Present the naive subclassing approach and its drawbacks.Step 3: Introduce the decorator pattern using composition and delegation.Step 4: Show how decorators can be stacked to combine behaviors.Step 5: Optionally, demonstrate parameterized decorators for dynamic customization.Step 6: Write clean, modular code and test with multiple decorators.

Time Allocation

Clarify: 2min → Approach: 5min → Code: 10min → Test: 3min. Total ~20min

What the Interviewer Tests

Understanding of composition over inheritance, ability to design flexible and maintainable code, knowledge of design patterns, and clean delegation.

Common Follow-ups

  • How to remove a decorator at runtime? → Keep references and unwrap as needed.
  • Can decorators change the interface? → No, they must conform to the component interface.
  • Difference between decorator and proxy? → Decorator adds behavior, proxy controls access.
  • How to implement decorators in languages without interfaces? → Use duck typing or abstract base classes.
💡 These follow-ups test deeper understanding of the pattern's flexibility, limitations, and differences from related patterns.
🔍
Pattern Recognition

When to Use

1. Need to add responsibilities to objects dynamically. 2. Avoid subclass explosion from combinatorial feature additions. 3. Want to extend behavior transparently without changing existing code. 4. Require flexible stacking or chaining of behaviors.

Signature Phrases

add responsibilities to objects dynamicallywrap an object to extend behavioravoid subclass explosion

NOT This Pattern When

Inheritance-based extension, Strategy pattern (encapsulates algorithms), Chain of Responsibility (passes requests)

Similar Problems

Proxy Pattern - controls access but does not add behaviorAdapter Pattern - changes interface rather than adding behavior

Practice

(1/5)
1. You need to design a system where user actions can be executed, undone, and redone, while also supporting queuing of requests and logging of all operations. Which design approach best fits this requirement to ensure loose coupling between the invoker and the receiver and to enable undo/redo functionality?
easy
A. Implementing the Command Pattern where each action is encapsulated as an object with execute and undo methods, and the invoker manages undo and redo stacks.
B. Using a simple function call approach where each action directly modifies the receiver's state without encapsulation.
C. Applying a greedy algorithm that chooses the next best action to execute without storing history.
D. Using a dynamic programming approach to store all possible states and transitions for undo/redo.

Solution

  1. Step 1: Identify the need for encapsulation of actions

    The problem requires actions to be undoable and redoable, which demands encapsulating each action's execution and reversal logic.
  2. Step 2: Recognize the pattern that supports undo/redo and request queuing

    The Command Pattern encapsulates requests as objects, allowing the invoker to queue, log, and manage undo/redo stacks independently from the receiver.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Command Pattern enables undo/redo with encapsulated commands [OK]
Hint: Undo/redo needs encapsulated commands, not direct calls [OK]
Common Mistakes:
  • Thinking direct function calls can support undo/redo cleanly
  • Confusing Command Pattern with greedy or DP algorithms
2. Consider the following code snippet implementing the Command Pattern with undo/redo stacks. After executing the commands: Add 5, Add 3, Undo, Redo, what is the final state of the receiver?
easy
A. 5
B. 8
C. 3
D. 0

Solution

  1. Step 1: Trace command executions

    Execute Add 5 -> state = 5; Execute Add 3 -> state = 8.
  2. Step 2: Trace undo and redo

    Undo removes Add 3 -> state reverts to 5; Redo reapplies Add 3 -> state returns to 8.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    State after redo is 8 [OK]
Hint: Undo then redo restores the last undone command [OK]
Common Mistakes:
  • Forgetting redo reapplies the command
  • Off-by-one in stack operations
3. In designing a Library Management System, which component is best suited to handle the tracking of book loans and returns to ensure accurate availability status?
easy
A. A separate LoanManager class that manages all loan and return transactions
B. The Library class, which maintains a global list of all books and their statuses
C. The User class, which tracks the books currently borrowed by the user
D. The Book class itself, by maintaining a status flag indicating availability

Solution

  1. Step 1: Understand Single Responsibility Principle

    Tracking loans and returns involves managing transactions and state changes, which is best encapsulated in a dedicated LoanManager to avoid bloating Book or User classes.
  2. Step 2: Why not Book class?

    Embedding availability logic in Book violates SRP and complicates concurrency handling.
  3. Step 3: Why not User class?

    User should track borrowed books but not manage loan state transitions globally.
  4. Step 4: Why not Library class?

    Library manages collections but delegating loan logic to a specialized manager improves modularity and concurrency control.
  5. Final Answer:

    Option A -> Option A
  6. Quick Check:

    LoanManager centralizes loan logic, enabling better consistency and easier concurrency handling.
Hint: Separate transaction logic from data entities for clarity and concurrency [OK]
Common Mistakes:
  • Putting loan logic inside Book class
  • Letting User class handle global loan state
  • Overloading Library class with transaction details
4. You have an object with nested mutable fields, and you need to create a new object instance such that changes to the nested fields in the new object do not affect the original. Which approach best guarantees this behavior?
easy
A. Manually copy only the top-level fields, sharing nested references to save memory
B. Use a greedy algorithm to selectively copy fields based on usage frequency
C. Serialize the object to JSON and deserialize it back to create a new instance
D. Recursively copy all nested objects to create independent duplicates

Solution

  1. Step 1: Understand the problem requirement

    The goal is to create a new object where nested mutable fields are independent, so changes in the copy do not affect the original.
  2. Step 2: Evaluate approaches

    Manual shallow copy (A) shares nested references, causing side effects. Greedy selective copying (B) is not a standard approach and risks missing nested objects. JSON serialization (D) can lose methods and fail on special objects. Recursive deep copy (C) ensures all nested objects are duplicated, preserving independence.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Deep copy duplicates nested objects -> no shared references [OK]
Hint: Deep copy duplicates nested objects fully [OK]
Common Mistakes:
  • Assuming shallow copy suffices for nested mutable fields
5. Suppose you have a legacy system that violates the Open/Closed Principle by frequently modifying core classes for new features. What is the best strategy to refactor it to comply with OCP without breaking existing functionality?
hard
A. Replace all core classes with interfaces and rewrite all implementations from scratch
B. Use global variables to control behavior changes instead of modifying classes
C. Continue modifying core classes but add feature flags to isolate changes
D. Introduce abstract base classes or interfaces and create new subclasses for new features, leaving existing classes unchanged

Solution

  1. Step 1: Understand refactoring goals

    Refactor to enable extension via new subclasses without modifying existing tested classes.
  2. Step 2: Why introducing abstract base classes or interfaces and creating new subclasses for new features, leaving existing classes unchanged fits

    Introducing abstractions and subclassing allows adding features by extension, preserving existing code.
  3. Step 3: Why other options fail

    Replace all core classes with interfaces and rewrite all implementations from scratch is costly and risky; Continue modifying core classes but add feature flags to isolate changes continues violating OCP; Use global variables to control behavior changes instead of modifying classes introduces global state, increasing complexity and bugs.
  4. Final Answer:

    Option D -> Option D
  5. Quick Check:

    Refactor by abstraction and extension, not rewrite or global state hacks.
Hint: Refactor with abstractions and subclasses, keep existing code intact [OK]
Common Mistakes:
  • Thinking rewrite is the only way to comply with OCP
  • Believing feature flags solve OCP violations
  • Using global variables as a workaround
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard