What is an example of the Liskov Substitution Principle?
Example of the Liskov Substitution Principle
The Liskov Substitution Principle (LSP) is one of the foundational principles in object-oriented programming and part of the SOLID principles. LSP states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In simpler terms, if class S
is a subtype of class T
, then objects of type T
should be replaceable with objects of type S
without altering the desirable properties of the program (e.g., correctness, task performed).
Understanding Liskov Substitution Principle
Key Points:
- Substitutability: Subclasses should be substitutable for their base classes.
- Behavior Preservation: The behavior of the program should remain consistent when a subclass replaces a base class.
- No Strengthened Preconditions: Subclasses should not impose stricter conditions than their base classes.
- No Weakened Postconditions: Subclasses should not promise less than their base classes.
Practical Example in Python
Let's consider a classic example involving geometric shapes: Rectangle
and Square
.
Base Class: Rectangle
class Rectangle: def __init__(self, width, height): self.width = width self.height = height def set_width(self, width): self.width = width def set_height(self, height): self.height = height def get_area(self): return self.width * self.height
Subclass: Square
class Square(Rectangle): def set_width(self, width): self.width = width self.height = width # Ensuring square property def set_height(self, height): self.height = height self.width = height # Ensuring square property
Function Utilizing Rectangle
def process_rectangle(rect): rect.set_width(5) rect.set_height(10) assert rect.get_area() == 50, "Area should be 50"
Using Rectangle
rect = Rectangle(2, 3) process_rectangle(rect) # Passes assertion print(f"Rectangle Area: {rect.get_area()}") # Output: Rectangle Area: 50
Using Square (Violation of LSP)
square = Square(2, 2) process_rectangle(square) # Assertion fails print(f"Square Area: {square.get_area()}") # Output: Square Area: 100
Explanation:
-
Expected Behavior:
- For a
Rectangle
with width5
and height10
, the area should be50
.
- For a
-
Using
Rectangle
:- The
process_rectangle
function sets the width to5
and height to10
. - The area correctly computes to
50
, passing the assertion.
- The
-
Using
Square
:- The
process_rectangle
function sets the width to5
, which also sets the height to5
due to the overridden methods inSquare
. - Then, it sets the height to
10
, which also sets the width to10
. - The expected area was
50
, but the actual area is100
, causing the assertion to fail.
- The
Violation of LSP:
- The
Square
class alters the expected behavior of theRectangle
class by enforcing equal width and height. This change leads to unexpected results when aSquare
is used in place of aRectangle
, violating the Liskov Substitution Principle.
Correcting the Violation
To adhere to LSP, ensure that subclasses do not alter the expected behavior of their base classes. One way to resolve this is by avoiding inheritance in cases where it doesn't make sense.
Using Composition Instead of Inheritance
Instead of having Square
inherit from Rectangle
, use composition to include a Rectangle
within Square
.
class Square: def __init__(self, side_length): self.rectangle = Rectangle(side_length, side_length) def set_side(self, side_length): self.rectangle.set_width(side_length) self.rectangle.set_height(side_length) def get_area(self): return self.rectangle.get_area()
Updated Function
def process_shape(shape): if isinstance(shape, Rectangle): shape.set_width(5) shape.set_height(10) assert shape.get_area() == 50, "Area should be 50"
Using Rectangle and Square
rect = Rectangle(2, 3) process_shape(rect) # Passes assertion print(f"Rectangle Area: {rect.get_area()}") # Output: Rectangle Area: 50 square = Square(2) process_shape(square.rectangle) # Passes assertion print(f"Square Area: {square.get_area()}") # Output: Square Area: 50
Explanation:
- By using composition,
Square
manages its own behavior without altering theRectangle
class. - The
process_shape
function remains compatible withRectangle
instances. - This approach maintains the substitutability without violating LSP.
Additional Example: Payment System
Base Class: Payment
class Payment: def pay(self, amount): raise NotImplementedError("Subclasses should implement this!")
Subclass: CreditCardPayment
class CreditCardPayment(Payment): def pay(self, amount): print(f"Processing credit card payment of ${amount}")
Subclass: PayPalPayment
class PayPalPayment(Payment): def pay(self, amount): print(f"Processing PayPal payment of ${amount}")
Function Utilizing Payment
def process_payment(payment_method, amount): payment_method.pay(amount) print("Payment processed successfully.")
Using Subclasses
credit_card = CreditCardPayment() paypal = PayPalPayment() process_payment(credit_card, 100) # Output: # Processing credit card payment of $100 # Payment processed successfully. process_payment(paypal, 200) # Output: # Processing PayPal payment of $200 # Payment processed successfully.
Explanation:
- Both
CreditCardPayment
andPayPalPayment
are subclasses ofPayment
. - They implement the
pay
method as required. - The
process_payment
function can seamlessly work with any subclass ofPayment
, adhering to LSP.
Summary
The Liskov Substitution Principle ensures that subclasses can stand in for their base classes without disrupting the behavior of the program. By adhering to LSP, you create more robust, maintainable, and flexible code. Avoid scenarios where subclasses alter the expected behavior of base classes, and consider using composition over inheritance when appropriate.
For a deeper understanding and more examples of the Liskov Substitution Principle and other object-oriented design principles, consider exploring the Grokking the Object Oriented Design Interview course on DesignGurus.io.
Happy Coding!
GET YOUR FREE
Coding Questions Catalog