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. In designing a parking lot system using OOP, which component is best suited to decide which type of parking spot (e.g., compact, large, handicapped) should be allocated to an incoming vehicle?
easy
A. The ParkingLot class, as it manages all spots and vehicles
B. The ParkingSpot class, since it represents the spot's characteristics
C. The Vehicle class, because it knows its own size and type
D. A Factory or Strategy pattern component that encapsulates the allocation logic

Solution

  1. Step 1: Understand responsibilities

    The Vehicle class only knows about itself, not allocation rules. The ParkingSpot class represents a spot but doesn't decide allocation. The ParkingLot manages overall state but delegating allocation logic to a dedicated component improves modularity.
  2. Step 2: Recognize design pattern role

    The Factory or Strategy pattern encapsulates allocation logic, allowing easy extension and modification without changing core classes.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Allocation logic centralized -> easier to maintain and extend [OK]
Hint: Allocation logic belongs in a dedicated pattern component, not core entities [OK]
Common Mistakes:
  • Assigning allocation responsibility to Vehicle or ParkingSpot classes
  • Putting all logic inside ParkingLot class leading to tight coupling
2. In the object-oriented design of a Snake and Ladder game, which component is primarily responsible for managing the state transitions of a player's position after a dice roll?
easy
A. The Dice class, since it generates the number that determines movement
B. The Player class, as it holds the current position and updates it directly
C. The Board class, because it contains the snakes and ladders and applies their effects
D. The GameController class, which orchestrates the game flow and updates player positions accordingly

Solution

  1. Step 1: Understand the role of Dice

    The Dice only generates a random number; it does not manage state transitions.
  2. Step 2: Consider Player class responsibilities

    Player holds position but should not decide how to update it considering snakes or ladders.
  3. Step 3: Analyze Board class role

    Board knows snakes and ladders but does not manage player state transitions directly.
  4. Step 4: Role of GameController

    GameController coordinates dice roll, queries Board for snakes/ladders, and updates Player position accordingly.
  5. Final Answer:

    Option D -> Option D
  6. Quick Check:

    GameController centralizes state transitions, ensuring separation of concerns.
Hint: GameController orchestrates state changes, not Dice or Player alone [OK]
Common Mistakes:
  • Thinking Dice manages player position
  • Assuming Player updates position without Board's input
  • Believing Board directly changes player state
3. You are designing a system that manages user accounts and sends notification emails. According to the Single Responsibility Principle, how should you organize these responsibilities?
easy
A. Separate user account management and email notification into different classes because each has a different reason to change.
B. Combine user account management and email notification in one class because they are related to users.
C. Put all user-related functionalities, including notifications, into a single class to reduce the number of classes.
D. Create one class for user management and embed email notification logic inside its methods to simplify interactions.

Solution

  1. Step 1: Identify reasons to change

    User account management changes when user data or authentication changes; email notifications change when messaging or delivery requirements change.
  2. Step 2: Apply SRP

    Since these reasons to change differ, they should be separated into different classes to avoid coupling unrelated changes.
  3. Step 3: Evaluate other options

    Options A, C, and D combine responsibilities, increasing coupling and reducing cohesion, violating SRP.
  4. Final Answer:

    Option A -> Option A
  5. Quick Check:

    Separate classes for distinct reasons to change -> SRP compliant.
Hint: One class, one reason to change.
Common Mistakes:
  • Assuming related domain means same responsibility.
  • Combining functionalities to reduce class count.
  • Embedding multiple responsibilities for convenience.
4. Which of the following is a common trade-off or limitation when favoring composition over inheritance in object-oriented design?
medium
A. Composition always leads to more complex code and harder maintenance than inheritance
B. Composition can increase the number of objects and indirection, potentially impacting performance
C. Composition prevents code reuse since behaviors cannot be shared
D. Composition forces tight coupling between composed objects

Solution

  1. Step 1: Evaluate each option's validity

    Composition always leads to more complex code and harder maintenance than inheritance is false; composition often improves maintainability. Composition prevents code reuse since behaviors cannot be shared is false; composition promotes code reuse via behavior objects. Composition forces tight coupling between composed objects is false; composition reduces coupling by separating concerns.
  2. Step 2: Understand the trade-off

    Composition introduces more objects and delegation layers, which can add runtime overhead and complexity in object management.
  3. Step 3: Confirm correct trade-off

    Composition can increase the number of objects and indirection, potentially impacting performance correctly identifies the potential performance and complexity cost of increased indirection.
  4. Final Answer:

    Option B -> Option B
  5. Quick Check:

    Composition trades off some runtime overhead for flexibility and maintainability.
Hint: Composition trades flexibility for some performance overhead, not complexity or coupling.
Common Mistakes:
  • Believing composition always simplifies code without cost
  • Thinking composition prevents code reuse
5. What is the time complexity of accessing the singleton instance using the double-checked locking pattern in a multithreaded environment after the instance has been initialized?
medium
A. O(1) because synchronization is bypassed after initialization
B. O(log n) due to locking mechanisms involved
C. O(n) due to synchronization overhead on every access
D. O(n^2) if multiple threads contend simultaneously

Solution

  1. Step 1: Understand double-checked locking behavior

    Synchronization occurs only during the first initialization; subsequent calls skip locking due to the volatile check.
  2. Step 2: Analyze time complexity after initialization

    After initialization, the instance is returned immediately without synchronization, so access is constant time.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Locking overhead is avoided after first init, so O(1) access [OK]
Hint: Locking only on first access -> O(1) after init [OK]
Common Mistakes:
  • Assuming synchronization on every call
  • Confusing locking cost with algorithmic complexity
  • Overestimating contention impact
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