Bird
Raised Fist0
Interview Prepoop-design-patternsmediumAmazonGoogleFlipkartCREDRazorpay

Liskov Substitution Principle - Subtype Behavioural Contract

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 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 fillAnswer 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 have a legacy payment processing system with an incompatible interface, and you want to integrate it into a new e-commerce platform without changing the legacy code. Which pattern best suits this scenario?
easy
A. Facade, to provide a simplified interface to the legacy system
B. Adapter, to convert the legacy interface to the new platform's expected interface
C. Proxy, to control access and add security to the legacy system
D. Decorator, to add new behavior to the legacy system dynamically

Solution

  1. Step 1: Identify the problem

    The legacy system's interface is incompatible with the new platform.
  2. Step 2: Understand pattern intents

    Adapter converts one interface to another, enabling integration without changing legacy code. Facade simplifies a complex subsystem but doesn't change interfaces. Proxy controls access, not interface compatibility. Decorator adds behavior dynamically, unrelated to interface mismatch.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    Adapter is the go-to pattern for interface incompatibility issues.
Hint: Adapter = interface translator; Facade = interface simplifier; Proxy = access controller
Common Mistakes:
  • Confusing Facade with Adapter because both provide a new interface
  • Thinking Proxy changes interfaces rather than controlling access
  • Assuming Decorator handles interface incompatibility
2. Given the following Python code using the Decorator Pattern, what is the output of print(coffee.description()) and print(coffee.cost()) after wrapping a SimpleCoffee with MilkDecorator (amount=2) and then SugarDecorator (amount=1)?
easy
A. Coffee, Milk(2), Sugar(1) 6.3
B. Coffee, Sugar(1), Milk(2) 5.8
C. Coffee, Sugar(1), Milk(2) 6.3
D. Coffee, Milk(2), Sugar(1) 5.8

Solution

  1. Step 1: Trace description calls

    Starting from SugarDecorator: description() calls MilkDecorator.description(), which calls SimpleCoffee.description() returning "Coffee". Then MilkDecorator adds ", Milk(2)", SugarDecorator adds ", Sugar(1)" -> "Coffee, Milk(2), Sugar(1)".
  2. Step 2: Trace cost calls

    SimpleCoffee.cost() = 5. MilkDecorator adds 0.5 * 2 = 1. SugarDecorator adds 0.3 * 1 = 0.3. Total cost = 5 + 1 + 0.3 = 6.3.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Description order matches wrapping order; cost sums correctly [OK]
Hint: Decorator calls chain in wrapping order [OK]
Common Mistakes:
  • Mixing order of decorators in description
  • Forgetting to multiply cost by amount
3. 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
4. What is the time complexity of constructing a complex object using the Builder pattern when the object has k parts to build?
medium
A. O(k), because each of the k parts is built sequentially
B. O(1), since each build method is called once
C. O(k^2), due to nested calls between builder methods
D. O(log k), as parts are built using divide-and-conquer

Solution

  1. Step 1: Identify number of build steps

    The Builder pattern calls a build method for each part, so k parts mean k method calls.
  2. Step 2: Analyze time per build call

    Each build method adds a part in O(1) time, so total time is O(k).
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Linear time proportional to number of parts built [OK]
Hint: Each part built once -> O(k) time [OK]
Common Mistakes:
  • Confusing constant time with O(k)
  • Assuming nested calls cause O(k^2)
5. In a legacy system, a class handles both business logic and logging. You want to refactor it following SRP, but the logging code is tightly intertwined with business logic. What is the best approach to refactor this while respecting SRP?
hard
A. Ignore SRP in this case because legacy code should not be refactored.
B. Leave logging inside the class because extracting it would break existing functionality and increase risk.
C. Merge business logic and logging into a utility class to centralize all cross-cutting concerns.
D. Extract logging into a separate class and replace logging calls with calls to this new class, even if it requires modifying many places.

Solution

  1. Step 1: Identify responsibilities

    Business logic and logging are separate reasons to change.
  2. Step 2: Refactor strategy

    Extract logging into its own class to isolate changes related to logging.
  3. Step 3: Address tight coupling

    Though intertwined, refactoring calls to the new logging class improves maintainability and respects SRP.
  4. Step 4: Evaluate other options

    Options B and D avoid refactoring, risking future issues; C increases coupling by merging unrelated concerns.
  5. Final Answer:

    Option D -> Option D
  6. Quick Check:

    Extract and isolate responsibilities even if it requires effort -> SRP compliance.
Hint: Separate concerns even if intertwined; refactor stepwise.
Common Mistakes:
  • Avoiding refactoring due to perceived risk.
  • Merging unrelated concerns for convenience.
  • Ignoring SRP in legacy code.