Bird
Raised Fist0
Interview Prepoop-design-patternsmediumAmazonGoogleMicrosoftFlipkartSwiggyRazorpay

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

Choose your preparation mode3 modes available
▶
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 fill★Answer 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. 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
2. You need to traverse a tree-like structure where each node can be either a single element or a composite containing multiple child elements. The traversal should allow clients to iterate over all elements uniformly without exposing the internal structure. Which design approach best fits this requirement?
easy
A. Implement the Composite pattern combined with an Iterator interface to provide a unified traversal abstraction.
B. Use a recursive brute force traversal that manually visits each node and its children.
C. Apply a greedy algorithm that visits nodes based on a heuristic to minimize traversal time.
D. Use dynamic programming to store and reuse traversal results for overlapping subtrees.

Solution

  1. Step 1: Understand the problem structure

    The problem involves a tree with nodes that can be leaves or composites containing children, requiring uniform traversal.
  2. Step 2: Identify the suitable pattern

    The Composite pattern allows treating individual objects and compositions uniformly, and combining it with an Iterator abstracts traversal details.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Composite + Iterator provides uniform traversal abstraction [OK]
Hint: Composite + Iterator unifies traversal of tree structures [OK]
Common Mistakes:
  • Confusing traversal with greedy or DP approaches
  • Using manual recursion without abstraction
3. Examine the following buggy Invoker code snippet implementing undo/redo stacks. Which line contains the subtle bug that can cause incorrect redo behavior after executing a new command?
medium
A. Line where redo_stack is not cleared after new command execution
B. Line where redo_stack is appended in undo
C. Line where command.execute() is called in execute_command
D. Line where undo_stack is popped in undo

Solution

  1. Step 1: Identify expected redo stack behavior

    After executing a new command, redo stack must be cleared to avoid invalid redo commands.
  2. Step 2: Locate missing redo_stack.clear()

    The execute_command method lacks clearing redo_stack, so redo commands remain stale.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Redo stack not cleared after new command breaks redo correctness [OK]
Hint: Redo stack must clear on new command execution [OK]
Common Mistakes:
  • Forgetting to clear redo stack
  • Undoing commands in wrong order
4. Identify the bug in the following Python code implementing deep copy for the Profile class:
import copy

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

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

original = Profile('Alice', [10, 20])
copy_obj = copy.deepcopy(original)
copy_obj.scores.append(30)
print(original.scores)
medium
A. Line assigning new_name = self.name does not deepcopy the name string
B. Line assigning new_scores = copy.deepcopy(self.scores, memo) incorrectly copies scores
C. The __init__ method does not initialize scores properly
D. The return statement returns a new Profile instead of modifying self

Solution

  1. Step 1: Examine __deepcopy__ method

    The line new_name = self.name copies the reference to the name string instead of deep copying it.
  2. Step 2: Understand impact

    Strings are immutable in Python, so shallow copy is usually safe, but if name were a mutable object, this would cause shared references and bugs. Proper deep copy should be used for consistency.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Only new_name assignment lacks deepcopy -> subtle bug [OK]
Hint: All nested fields must be deep copied consistently [OK]
Common Mistakes:
  • Forgetting to deepcopy all nested fields, assuming immutables are safe
5. Identify the bug in the following code snippet implementing the strategy pattern for payment processing:
class PaymentProcessor:
    def __init__(self, strategy: PaymentStrategy):
        self.strategy = strategy

    def pay(self, amount):
        if isinstance(self.strategy, CreditCardStrategy):
            print(f"Processing credit card payment of ${amount}")
        elif isinstance(self.strategy, UPIStrategy):
            print(f"Processing UPI payment of ${amount}")
        else:
            print("Invalid payment method")
medium
A. The bug is that the pay method uses conditional checks instead of polymorphism.
B. The bug is missing a call to the strategy's pay method inside pay().
C. The bug is in the constructor where the strategy is not assigned properly.
D. The bug is that the else clause should raise an exception instead of printing.

Solution

  1. Step 1: Analyze the pay method implementation

    The pay method uses isinstance checks and conditionals to decide behavior, which defeats the purpose of polymorphism.
  2. Step 2: Identify correct polymorphic usage

    The correct approach is to call self.strategy.pay(amount) directly, letting each strategy handle its own payment logic.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Using conditionals in pay() breaks the strategy pattern [OK]
Hint: Strategy pattern requires polymorphic calls, not conditionals [OK]
Common Mistakes:
  • Using type checks instead of polymorphism
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