Bird
Raised Fist0
Interview Prepoop-design-patternsmediumAmazonGoogleFlipkartCREDRazorpay

Liskov Substitution Principle - Subtype Behavioural Contract

Choose your preparation mode3 modes available
▶
Steps
setup

Define Base Class Shape

We define the abstract base class Shape with methods to set width and height and to calculate area. This sets the contract that all shapes must follow.

💡 Defining the base class establishes the expected interface and behavior that subtypes must honor.
Line:class Shape: def set_width(self, width): pass def set_height(self, height): pass def area(self): pass
💡 The base class defines the behavioral contract for all shapes.
📊
Liskov Substitution Principle - Subtype Behavioural Contract - Watch the Algorithm Execute, Step by Step
Watching this step-by-step helps you understand how subtype behavioral contracts ensure substitutability, which is hard to grasp from code alone.
Step 1/10
·Active fill★Answer cell
Defines the abstract base class with the behavioral contract
«abstract»Shape
+set_width()
+set_height()
+area()
Rectangle implements the Shape contract
«abstract»Shape
+set_width()
+set_height()
+area()
Rectangle
−width: int
−height: int
+__init__()
+set_width()
+set_height()
+1 more
Rectangle ▷ Shape (1:1)
Square violates the behavioral contract by changing setter semantics
«abstract»Shape
+set_width()
+set_height()
+area()
Rectangle
−width: int
−height: int
+__init__()
+set_width()
+set_height()
+1 more
Square
−size: int
+__init__()
+set_width()
+set_height()
+1 more
Rectangle ▷ Shape (1:1)Square ▷ Shape (1:1)
⚠ Violates Liskov Substitution Principle: Square changes expected behavior of setters
Rectangle instance with width and height set independently
Rectangle
−width: int
−height: int
+set_width()
+set_height()
+area()
Rectangle ▷ Shape (1:1)
Rectangle area method returns width * height = 50
Rectangle
−width: int
−height: int
+area()
Rectangle ▷ Shape (1:1)
Square instance with size set to 5 via set_width
Square
−size: int
+set_width()
+set_height()
+area()
Square ▷ Shape (1:1)
Square size updated to 10 via set_height
Square
−size: int
+set_width()
+set_height()
+area()
Square ▷ Shape (1:1)
Square area method returns size * size = 100
Square
−size: int
+area()
Square ▷ Shape (1:1)
⚠ Violates LSP: area inconsistent with independent width and height
Square used polymorphically as Shape/Rectangle
«abstract»Shape
+set_width()
+set_height()
+area()
Square
−size: int
+set_width()
+set_height()
+area()
Square ▷ Shape (1:1)
⚠ Violates LSP: substitution breaks expected behavior
Final state shows behavioral contract violation
Square
−size: int
+area()
Square ▷ Shape (1:1)
⚠ LSP violation: Square does not behave as Rectangle

Key Takeaways

✓ Subtypes must preserve the behavioral contract of their base types to satisfy the Liskov Substitution Principle.

This is hard to see from code alone because method signatures may match but behavior can differ subtly.

✓ Violating the contract leads to unexpected results when subtypes are used polymorphically.

Visualizing the substitution and its effect on state and output clarifies why this is problematic.

✓ Designing subtypes requires careful attention to not only interface but also semantics and side effects.

The trace shows concretely how Square's setters break expectations, a subtle but critical insight.

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. Given the following Python code using the Builder pattern, what is the output of print(house) after constructing a basic house with the Director?
easy
A. House parts: Walls, Roof
B. House parts: Walls, Roof, Pool
C. House parts: Roof, Walls
D. House parts: Pool

Solution

  1. Step 1: Trace Director.construct_basic_house()

    The Director calls build_walls() and build_roof() on the builder, adding "Walls" and "Roof" to the house parts.
  2. Step 2: Check the final house parts list

    The house parts list contains ["Walls", "Roof"]. The pool is not added in this method.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Only walls and roof are added in basic house construction [OK]
Hint: Basic house adds walls and roof only [OK]
Common Mistakes:
  • Assuming pool is added by default
  • Mixing order of parts
3. When a new feature is added by extending a class hierarchy following the Open/Closed Principle, what is the typical sequence of events when the system executes a method call on the new subclass instance?
easy
A. The base class method is always executed first, then the subclass method overrides it afterward
B. The system duplicates the base class code inside the subclass to avoid modifying the base
C. The subclass method is invoked directly due to polymorphism, without modifying base class code
D. Both base and subclass methods execute sequentially because the base class is modified to call the subclass

Solution

  1. Step 1: Understand polymorphic dispatch

    Method calls on subclass instances invoke the subclass's overridden method directly.
  2. Step 2: Base class code remains unchanged

    The base class is closed for modification; no changes are made to call subclass methods explicitly.
  3. Step 3: Why other options are incorrect

    The base class method is always executed first, then the subclass method overrides it afterward incorrectly suggests base method runs first then subclass; Both base and subclass methods execute sequentially because the base class is modified to call the subclass implies base class modification; The system duplicates the base class code inside the subclass to avoid modifying the base suggests code duplication, violating DRY and OCP.
  4. Final Answer:

    Option C -> Option C
  5. Quick Check:

    Polymorphism enables extension without modifying base code.
Hint: Polymorphism calls subclass methods directly [OK]
Common Mistakes:
  • Assuming base method always runs first
  • Believing base class must be modified to support extension
  • Thinking code duplication is a valid OCP strategy
4. Imagine a class responsible for both data persistence and data validation. When a change in validation rules occurs, trace the impact on the class and explain what happens step-by-step.
easy
A. Only the validation methods need modification; persistence remains unaffected, so SRP is maintained.
B. Changing validation rules forces modifying the entire class, risking unintended side effects on persistence logic.
C. Validation changes automatically propagate to persistence without code changes due to tight coupling.
D. Persistence logic will break because validation and persistence are tightly integrated and inseparable.

Solution

  1. Step 1: Identify responsibilities

    The class handles both validation and persistence, two distinct reasons to change.
  2. Step 2: Trace change impact

    Changing validation rules requires modifying validation code inside the class.
  3. Step 3: Side effects

    Because persistence logic shares the class, changes risk affecting persistence unintentionally, increasing maintenance risk.
  4. Step 4: SRP violation

    This coupling violates SRP, as one reason to change (validation) affects unrelated functionality (persistence).
  5. Final Answer:

    Option B -> Option B
  6. Quick Check:

    One reason to change should not force changes in unrelated code -> SRP violation.
Hint: One reason to change means one place to modify.
Common Mistakes:
  • Assuming changes affect only related methods without side effects.
  • Believing tight coupling is harmless if code is in one class.
  • Thinking validation and persistence are always linked.
5. You need to ensure that a class has only one instance throughout the application lifecycle, and this instance must be lazily initialized in a thread-safe manner without incurring synchronization overhead on every access. Which design approach best fits this requirement?
easy
A. Use a simple static variable without synchronization and rely on the language's memory model.
B. Use a synchronized method that creates the instance on first call, locking every time.
C. Create the instance eagerly at class loading time to avoid synchronization issues.
D. Implement double-checked locking with a volatile instance variable to minimize synchronization overhead.

Solution

  1. Step 1: Understand the problem constraints

    The instance must be lazily initialized and thread-safe, but synchronization overhead should be minimized.
  2. Step 2: Evaluate each approach

    Synchronized method locks on every call, causing overhead. Eager initialization is thread-safe but not lazy. Unsynchronized static variable risks multiple instances in multithreaded contexts. Double-checked locking with volatile ensures lazy init, thread safety, and minimal locking.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Double-checked locking balances thread safety and performance [OK]
Hint: Double-checked locking minimizes synchronization overhead [OK]
Common Mistakes:
  • Assuming synchronized method is efficient enough
  • Believing eager initialization is always best
  • Ignoring volatile keyword necessity
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