Bird
Raised Fist0
Interview Prepoop-design-patternsmediumAmazonGoogleMicrosoftFlipkartSwiggyRazorpay

Open/Closed Principle - Open for Extension, Closed for Modification

Choose your preparation mode3 modes available

Start learning this pattern below

Jump into concepts and practice - no test required

or
Recommended
Test this pattern10 questions across easy, medium, and hard to know if this pattern is strong
Steps
setup

Define Abstract Shape Class

We start by defining an abstract base class 'Shape' that declares a method 'draw'. This class is abstract and cannot be instantiated directly.

💡 This step sets the foundation for extension by defining a contract that all shapes must follow.
Line:class Shape(ABC): @abstractmethod def draw(self): pass
💡 Abstract classes allow new shapes to be added without changing existing code, fulfilling the 'closed for modification' part.
📊
Open/Closed Principle - Open for Extension, Closed for Modification - Watch the Algorithm Execute, Step by Step
Watching this step-by-step helps you understand how the Open/Closed Principle is applied in practice, rather than just reading abstract definitions.
Step 1/10
·Active fillAnswer cell
Defines an abstract base class to enforce a common interface.
«abstract»Shape
+draw()
Concrete subclass implements abstract method to extend behavior.
«abstract»Shape
+draw()
Circle
+draw()
Circle Shape (1:1)
Another concrete subclass extends the abstract base class.
«abstract»Shape
+draw()
Circle
+draw()
Rectangle
+draw()
Circle Shape (1:1)Rectangle Shape (1:1)
Client class depends on abstraction, enabling extension without modification.
«abstract»Shape
+draw()
Circle
+draw()
Rectangle
+draw()
Drawing
shapes: List[Shape]
+__init__()
+add_shape()
+draw_all()
Circle Shape (1:1)Rectangle Shape (1:1)Drawing Shape (1:0..*)
Client holds references to abstract base class, enabling polymorphism.
«abstract»Shape
+draw()
Circle
+draw()
Rectangle
+draw()
Drawing
shapes: List[Shape]
+__init__()
+add_shape()
+draw_all()
Circle Shape (1:1)Rectangle Shape (1:1)Drawing Shape (1:0..*)
Client aggregates multiple shape instances polymorphically.
«abstract»Shape
+draw()
Circle
+draw()
Rectangle
+draw()
Drawing
shapes: List[Shape]
+__init__()
+add_shape()
+draw_all()
Circle Shape (1:1)Rectangle Shape (1:1)Drawing Shape (1:0..*)
Polymorphic method calls enable extension without modification.
«abstract»Shape
+draw()
Circle
+draw()
Rectangle
+draw()
Drawing
shapes: List[Shape]
+__init__()
+add_shape()
+draw_all()
Circle Shape (1:1)Rectangle Shape (1:1)Drawing Shape (1:0..*)
Extending system by adding new subclass without modifying existing classes.
«abstract»Shape
+draw()
Circle
+draw()
Rectangle
+draw()
Triangle
+draw()
Drawing
shapes: List[Shape]
+__init__()
+add_shape()
+draw_all()
Circle Shape (1:1)Rectangle Shape (1:1)Triangle Shape (1:1)Drawing Shape (1:0..*)
New subclass instances integrate with existing client code.
«abstract»Shape
+draw()
Circle
+draw()
Rectangle
+draw()
Triangle
+draw()
Drawing
shapes: List[Shape]
+__init__()
+add_shape()
+draw_all()
Circle Shape (1:1)Rectangle Shape (1:1)Triangle Shape (1:1)Drawing Shape (1:0..*)
Polymorphism enables seamless extension of behavior.
«abstract»Shape
+draw()
Circle
+draw()
Rectangle
+draw()
Triangle
+draw()
Drawing
shapes: List[Shape]
+__init__()
+add_shape()
+draw_all()
Circle Shape (1:1)Rectangle Shape (1:1)Triangle Shape (1:1)Drawing Shape (1:0..*)

Key Takeaways

The Open/Closed Principle is implemented by defining an abstract base class and extending it with new subclasses without modifying existing code.

This insight is hard to see from code alone because the principle is about design intent and extensibility, which is clearer when visualized step-by-step.

Client code depends on abstractions (the Shape interface) rather than concrete implementations, enabling polymorphism.

Seeing the client class interact only with the abstract class clarifies how extension is possible without modification.

Adding new functionality (like the Triangle class) requires no changes to existing classes or client code, demonstrating maintainability.

The visualization shows how new subclasses integrate seamlessly, which is difficult to grasp by reading code alone.

Practice

(1/5)
1. You are designing a system for different types of vehicles where some can fly and some can float on water. Which design approach best supports adding new capabilities like flying or floating without modifying existing vehicle classes?
easy
A. Use inheritance to create subclasses like FlyingCar and FloatingCar from Vehicle
B. Use a single Vehicle class with flags indicating if it can fly or float
C. Use composition by creating separate capability classes like FlyBehavior and FloatBehavior and compose them with Vehicle
D. Use inheritance and override methods in each subclass to add flying or floating behavior

Solution

  1. Step 1: Identify the problem with inheritance here

    Inheritance leads to a combinatorial explosion of subclasses (FlyingCar, FloatingCar, FlyingFloatingCar), making the design rigid and hard to maintain.
  2. Step 2: Understand composition benefits

    Composition allows attaching behaviors dynamically via separate classes (FlyBehavior, FloatBehavior), promoting flexibility and easier extension.
  3. Step 3: Analyze other options

    Options A and C rely on inheritance, causing tight coupling and poor scalability. Use a single Vehicle class with flags indicating if it can fly or float uses flags, which leads to complex conditional logic and violates single responsibility.
  4. Final Answer:

    Option C -> Option C
  5. Quick Check:

    Composition supports adding new capabilities without modifying existing classes, enhancing maintainability and flexibility.
Hint: Composition lets you add capabilities by combining behaviors, inheritance forces rigid hierarchies.
Common Mistakes:
  • Assuming inheritance is always better for code reuse
  • Using flags instead of behaviors leads to messy conditionals
2. When a class inherits from multiple classes that have a method with the same name, describe the step-by-step process the Method Resolution Order (MRO) uses to determine which method is called.
easy
A. MRO uses a linearization algorithm that merges the order of parents and their ancestors to find the method.
B. MRO searches the first parent class fully before moving to the next parent class.
C. MRO always calls the method from the last parent class listed in the inheritance.
D. MRO randomly picks the method from any parent class that defines it.

Solution

  1. Step 1: Understand naive search

    MRO does not simply search the first parent class fully before moving to the next; it uses a more sophisticated approach.
  2. Step 2: Recognize MRO linearization

    MRO uses a specific linearization (like C3 linearization) that merges parent classes and their ancestors in a consistent order.
  3. Step 3: Eliminate incorrect options

    MRO always calls the method from the last parent class listed in the inheritance is incorrect because the last parent is not always chosen; order and ancestors matter. MRO randomly picks the method from any parent class that defines it is incorrect because MRO is deterministic, not random.
  4. Final Answer:

    Option A -> Option A
  5. Quick Check:

    MRO merges inheritance hierarchies to find the correct method in a predictable order.
Hint: MRO = deterministic linearization of inheritance graph
Common Mistakes:
  • Assuming simple left-to-right search suffices
  • Believing last parent always overrides
  • Thinking method choice is random
3. Given the following Python code using deep copy, what will be printed after modifying the copy's scores list?
import copy

class Profile:
    def __init__(self, name, scores):
        self.name = name
        self.scores = scores

    def __deepcopy__(self, memo):
        new_name = copy.deepcopy(self.name, memo)
        new_scores = copy.deepcopy(self.scores, memo)
        return Profile(new_name, new_scores)

original = Profile('Alice', [10, 20])
copy_obj = copy.deepcopy(original)
print('Original scores:', original.scores)
print('Copy scores:', copy_obj.scores)
copy_obj.scores.append(30)
print('After modifying copy scores:')
print('Original scores:', original.scores)
print('Copy scores:', copy_obj.scores)
easy
A. Original scores: [10, 20] Copy scores: [10, 20] After modifying copy scores: Original scores: [10, 20, 30] Copy scores: [10, 20, 30]
B. Original scores: [10, 20] Copy scores: [10, 20] After modifying copy scores: Original scores: [10, 20] Copy scores: [10, 20, 30]
C. Original scores: [10, 20] Copy scores: [10, 20] After modifying copy scores: Original scores: [10, 20] Copy scores: [10, 20]
D. Original scores: [10, 20] Copy scores: [10, 20, 30] After modifying copy scores: Original scores: [10, 20] Copy scores: [10, 20, 30]

Solution

  1. Step 1: Trace initial print statements

    Both original.scores and copy_obj.scores start as [10, 20], so first two prints show identical lists.
  2. Step 2: Trace modification and final prints

    copy_obj.scores.append(30) modifies only the copy's scores list because deep copy created a new list. Original remains [10, 20]. Final prints reflect this separation.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    Deep copy prevents shared nested list -> original unchanged [OK]
Hint: Deep copy isolates nested mutable objects [OK]
Common Mistakes:
  • Assuming append affects original due to shared reference
4. What is the time complexity of computing the cost() method when stacking k decorators on a core object using the Decorator Pattern with dynamic behavior injection?
medium
A. O(1) because each decorator adds a fixed cost
B. O(k) because each decorator delegates the call to the next one
C. O(k^2) because each decorator calls all previous decorators recursively
D. O(log k) because decorators form a balanced tree structure

Solution

  1. Step 1: Identify call chain length

    Each decorator's cost() calls the wrapped object's cost(), forming a chain of length k.
  2. Step 2: Calculate total calls

    Cost computation requires traversing all k decorators once, so time complexity is O(k).
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    Each decorator adds constant work, total linear in k [OK]
Hint: Decorator calls chain length equals number of decorators [OK]
Common Mistakes:
  • Assuming O(1) because cost is a simple addition
  • Mistaking recursive calls as quadratic
5. Examine the following buggy code implementing the Template Method Pattern. Which line contains the subtle bug that breaks the pattern's intended behavior?
medium
A. Line overriding prepare_recipe in Tea subclass
B. Line defining abstract method brew in base class
C. Line calling add_condiments inside prepare_recipe base method
D. Line overriding customer_wants_condiments in Tea subclass

Solution

  1. Step 1: Identify overridden methods

    Tea overrides prepare_recipe, which breaks the template method pattern by duplicating and changing the algorithm flow.
  2. Step 2: Understand impact

    Overriding the template method in subclass bypasses the base class skeleton, causing inconsistent behavior and code duplication.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Template method must not be overridden by subclasses [OK]
Hint: Overriding template method breaks algorithm skeleton [OK]
Common Mistakes:
  • Thinking overriding abstract methods is bug
  • Ignoring hook method usage