Liskov Substitution Principle
The Liskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of its subclasses without breaking the application. In other words, derived classes must be substitutable for their base classes.
This principle was introduced by Barbara Liskov in 1987 and is a fundamental concept in object-oriented design.
Understanding the Principle
Section titled “Understanding the Principle”The Liskov Substitution Principle ensures that:
- Subclasses must honor the contract of their base class
- Subclasses should not weaken preconditions (requirements before a method runs)
- Subclasses should not strengthen postconditions (guarantees after a method runs)
- Subclasses should not throw new exceptions that the base class doesn’t throw
In simple terms: If it looks like a duck and quacks like a duck, it should behave like a duck!
Example 1: Rectangle and Square Problem
Section titled “Example 1: Rectangle and Square Problem”This is a classic example that demonstrates LSP violation. Let’s see why a Square should NOT inherit from Rectangle.
Violating LSP (Bad Approach)
Section titled “Violating LSP (Bad Approach)”class Rectangle: def __init__(self, width: float, height: float): self.width = width self.height = height
def set_width(self, width: float): """Setting width should only change width""" self.width = width
def set_height(self, height: float): """Setting height should only change height""" self.height = height
def get_area(self) -> float: return self.width * self.height
class Square(Rectangle): """❌ This violates LSP!""" def __init__(self, side: float): super().__init__(side, side) self.side = side
def set_width(self, width: float): """Problem: Setting width also changes height!""" self.width = width self.height = width # ❌ This breaks the contract! self.side = width
def set_height(self, height: float): """Problem: Setting height also changes width!""" self.height = height self.width = height # ❌ This breaks the contract! self.side = height
# This function expects Rectangle behaviordef test_rectangle(rect: Rectangle): """This function assumes Rectangle's contract""" initial_area = rect.get_area() rect.set_width(10) # Should only change width rect.set_height(5) # Should only change height
# Expected: width=10, height=5, area=50 # But with Square: width=5, height=5, area=25 ❌ return rect.get_area()
# Usage - This breaks!rectangle = Rectangle(5, 5)print(test_rectangle(rectangle)) # Works: 50
square = Square(5)print(test_rectangle(square)) # ❌ Breaks! Returns 25, not 50public class Rectangle { protected double width; protected double height;
public Rectangle(double width, double height) { this.width = width; this.height = height; }
// Setting width should only change width public void setWidth(double width) { this.width = width; }
// Setting height should only change height public void setHeight(double height) { this.height = height; }
public double getArea() { return width * height; }}
// ❌ This violates LSP!public class Square extends Rectangle { private double side;
public Square(double side) { super(side, side); this.side = side; }
// Problem: Setting width also changes height! @Override public void setWidth(double width) { this.width = width; this.height = width; // ❌ This breaks the contract! this.side = width; }
// Problem: Setting height also changes width! @Override public void setHeight(double height) { this.height = height; this.width = height; // ❌ This breaks the contract! this.side = height; }}
// This function expects Rectangle behaviorpublic class TestRectangle { // This function assumes Rectangle's contract public static double testRectangle(Rectangle rect) { double initialArea = rect.getArea(); rect.setWidth(10); // Should only change width rect.setHeight(5); // Should only change height
// Expected: width=10, height=5, area=50 // But with Square: width=5, height=5, area=25 ❌ return rect.getArea(); }
public static void main(String[] args) { Rectangle rectangle = new Rectangle(5, 5); System.out.println(testRectangle(rectangle)); // Works: 50.0
Square square = new Square(5); System.out.println(testRectangle(square)); // ❌ Breaks! Returns 25.0, not 50.0 }}Following LSP (Good Approach)
Section titled “Following LSP (Good Approach)”from abc import ABC, abstractmethod
class Shape(ABC): """Base class - defines the contract""" @abstractmethod def get_area(self) -> float: """Calculate and return the area""" pass
class Rectangle(Shape): """Rectangle honors Shape's contract""" def __init__(self, width: float, height: float): self.width = width self.height = height
def set_width(self, width: float): """Only changes width - maintains contract""" self.width = width
def set_height(self, height: float): """Only changes height - maintains contract""" self.height = height
def get_area(self) -> float: return self.width * self.height
class Square(Shape): """Square honors Shape's contract""" def __init__(self, side: float): self.side = side
def set_side(self, side: float): """Square-specific method""" self.side = side
def get_area(self) -> float: return self.side * self.side
# This function works with any Shapedef calculate_total_area(shapes: list[Shape]) -> float: """Works with any Shape subclass - LSP satisfied!""" total = 0 for shape in shapes: total += shape.get_area() # ✅ Both Rectangle and Square work! return total// Base class - defines the contractpublic abstract class Shape { // Calculate and return the area public abstract double getArea();}
// Rectangle honors Shape's contractpublic class Rectangle extends Shape { private double width; private double height;
public Rectangle(double width, double height) { this.width = width; this.height = height; }
// Only changes width - maintains contract public void setWidth(double width) { this.width = width; }
// Only changes height - maintains contract public void setHeight(double height) { this.height = height; }
@Override public double getArea() { return width * height; }}
// Square honors Shape's contractpublic class Square extends Shape { private double side;
public Square(double side) { this.side = side; }
// Square-specific method public void setSide(double side) { this.side = side; }
@Override public double getArea() { return side * side; }}
// This function works with any Shapepublic class ShapeCalculator { // Works with any Shape subclass - LSP satisfied! public static double calculateTotalArea(List<Shape> shapes) { double total = 0; for (Shape shape : shapes) { total += shape.getArea(); // ✅ Both Rectangle and Square work! } return total; }}Usage - Both work perfectly!
Section titled “Usage - Both work perfectly!”rectangle = Rectangle(5, 10) square = Square(5)
print(calculate_total_area([rectangle, square])) # ✅ Works: 75
</TabItem></Tabs>
**Why this follows LSP:**- Both `Rectangle` and `Square` honor the `Shape` contract- They can be used interchangeably where `Shape` is expected- No unexpected behavior changes
---
## Example 2: Payment Processing System
Consider a payment processing system where different payment methods need to be processed. Let's see how LSP violations can cause real-world problems.
### Violating LSP (Bad Approach)
```d2 sketchdirection: right
before: { label: "❌ Before (Violates LSP)"
PaymentProcessor: { shape: class
+process_payment(amount: float) +refund(transaction_id: str) }
CreditCardProcessor: { shape: class
+process_payment(amount: float) +refund(transaction_id: str) }
CryptocurrencyProcessor: { shape: class
+process_payment(amount: float) +refund(transaction_id: str) }
PaymentProcessor -> CreditCardProcessor: extends PaymentProcessor -> CryptocurrencyProcessor: extends
note: { shape: text style.italic: true CryptocurrencyProcessor\nbreaks the contract! }
CryptocurrencyProcessor -> note: {style.stroke-dash: 3}}class PaymentProcessor: """Base payment processor""" def process_payment(self, amount: float) -> str: """Process payment and return transaction ID""" # Base implementation transaction_id = f"TXN-{amount}" print(f"Processing payment of ${amount}") return transaction_id
def refund(self, transaction_id: str) -> bool: """Refund a transaction - should always work""" print(f"Refunding transaction {transaction_id}") return True
class CreditCardProcessor(PaymentProcessor): """Credit card payments - follows contract""" def process_payment(self, amount: float) -> str: transaction_id = f"CC-{amount}" print(f"Processing credit card payment of ${amount}") # Process credit card... return transaction_id
def refund(self, transaction_id: str) -> bool: print(f"Refunding credit card transaction {transaction_id}") # Refund credit card... return True
class CryptocurrencyProcessor(PaymentProcessor): """❌ Cryptocurrency payments - violates LSP!""" def process_payment(self, amount: float) -> str: transaction_id = f"CRYPTO-{amount}" print(f"Processing cryptocurrency payment of ${amount}") # Process crypto... return transaction_id
def refund(self, transaction_id: str) -> bool: """❌ Problem: Cryptocurrency refunds might not be possible!""" raise NotImplementedError("Cryptocurrency refunds are not supported!") # This breaks the contract - refund() should always work
# This function expects PaymentProcessor's contractdef process_refund(processor: PaymentProcessor, transaction_id: str): """This function assumes refund() always works""" try: success = processor.refund(transaction_id) if success: print("Refund successful!") else: print("Refund failed!") except Exception as e: print(f"Unexpected error: {e}") # ❌ Breaks when using CryptocurrencyProcessor!
# Usage - This breaks!cc_processor = CreditCardProcessor()crypto_processor = CryptocurrencyProcessor()
process_refund(cc_processor, "CC-100") # ✅ Worksprocess_refund(crypto_processor, "CRYPTO-100") # ❌ Breaks! Raises exception// Base payment processorpublic abstract class PaymentProcessor { // Process payment and return transaction ID public abstract String processPayment(double amount);
// Refund a transaction - should always work public boolean refund(String transactionId) { System.out.println("Refunding transaction " + transactionId); return true; }}
// Credit card payments - follows contractpublic class CreditCardProcessor extends PaymentProcessor { @Override public String processPayment(double amount) { String transactionId = "CC-" + amount; System.out.println("Processing credit card payment of $" + amount); // Process credit card... return transactionId; }
@Override public boolean refund(String transactionId) { System.out.println("Refunding credit card transaction " + transactionId); // Refund credit card... return true; }}
// ❌ Cryptocurrency payments - violates LSP!public class CryptocurrencyProcessor extends PaymentProcessor { @Override public String processPayment(double amount) { String transactionId = "CRYPTO-" + amount; System.out.println("Processing cryptocurrency payment of $" + amount); // Process crypto... return transactionId; }
// ❌ Problem: Cryptocurrency refunds might not be possible! @Override public boolean refund(String transactionId) { throw new UnsupportedOperationException("Cryptocurrency refunds are not supported!"); // This breaks the contract - refund() should always work }}
// This function expects PaymentProcessor's contractpublic class RefundProcessor { // This function assumes refund() always works public static void processRefund(PaymentProcessor processor, String transactionId) { try { boolean success = processor.refund(transactionId); if (success) { System.out.println("Refund successful!"); } else { System.out.println("Refund failed!"); } } catch (Exception e) { System.out.println("Unexpected error: " + e.getMessage()); // ❌ Breaks when using CryptocurrencyProcessor! } }
public static void main(String[] args) { // Usage - This breaks! CreditCardProcessor ccProcessor = new CreditCardProcessor(); CryptocurrencyProcessor cryptoProcessor = new CryptocurrencyProcessor();
processRefund(ccProcessor, "CC-100"); // ✅ Works processRefund(cryptoProcessor, "CRYPTO-100"); // ❌ Breaks! Throws exception }}Why this violates LSP:
- Code expecting
PaymentProcessorbreaks withCryptocurrencyProcessor refund()should always work, but crypto version throws exception- The contract is broken: refunds should be possible for all payment types
Following LSP (Good Approach)
Section titled “Following LSP (Good Approach)”from abc import ABC, abstractmethod
class PaymentProcessor(ABC): """Base class - all payments can be processed""" @abstractmethod def process_payment(self, amount: float) -> str: """Process payment and return transaction ID""" pass
class RefundableProcessor(PaymentProcessor): """Interface for processors that support refunds""" @abstractmethod def refund(self, transaction_id: str) -> bool: """Refund a transaction - only if processor supports it""" pass
class CreditCardProcessor(RefundableProcessor): """Credit card - supports both payment and refund""" def process_payment(self, amount: float) -> str: transaction_id = f"CC-{amount}" print(f"Processing credit card payment of ${amount}") return transaction_id
def refund(self, transaction_id: str) -> bool: print(f"Refunding credit card transaction {transaction_id}") return True
class CryptocurrencyProcessor(PaymentProcessor): """Cryptocurrency - only supports payment, no refunds""" def process_payment(self, amount: float) -> str: transaction_id = f"CRYPTO-{amount}" print(f"Processing cryptocurrency payment of ${amount}") return transaction_id # No refund() method - this is correct! Don't implement what you can't support
# Functions that work with specific contractsdef process_any_payment(processor: PaymentProcessor, amount: float): """Works with any PaymentProcessor""" return processor.process_payment(amount)
def process_refund(processor: RefundableProcessor, transaction_id: str): """Only works with RefundableProcessor - type-safe!""" return processor.refund(transaction_id)
# Usage - Type-safe and correct!cc_processor = CreditCardProcessor()crypto_processor = CryptocurrencyProcessor()
# Both can process paymentsprocess_any_payment(cc_processor, 100) # ✅ Worksprocess_any_payment(crypto_processor, 100) # ✅ Works
# Only refundable processors can refundprocess_refund(cc_processor, "CC-100") # ✅ Works# process_refund(crypto_processor, "CRYPTO-100") # ✅ Type error - prevents bugs!// Base class - all payments can be processedpublic abstract class PaymentProcessor { // Process payment and return transaction ID public abstract String processPayment(double amount);}
// Interface for processors that support refundspublic abstract class RefundableProcessor extends PaymentProcessor { // Refund a transaction - only if processor supports it public abstract boolean refund(String transactionId);}
// Credit card - supports both payment and refundpublic class CreditCardProcessor extends RefundableProcessor { @Override public String processPayment(double amount) { String transactionId = "CC-" + amount; System.out.println("Processing credit card payment of $" + amount); return transactionId; }
@Override public boolean refund(String transactionId) { System.out.println("Refunding credit card transaction " + transactionId); return true; }}
// Cryptocurrency - only supports payment, no refundspublic class CryptocurrencyProcessor extends PaymentProcessor { @Override public String processPayment(double amount) { String transactionId = "CRYPTO-" + amount; System.out.println("Processing cryptocurrency payment of $" + amount); return transactionId; } // No refund() method - this is correct! Don't implement what you can't support}
// Functions that work with specific contractspublic class PaymentService { // Works with any PaymentProcessor public static String processAnyPayment(PaymentProcessor processor, double amount) { return processor.processPayment(amount); }
// Only works with RefundableProcessor - type-safe! public static boolean processRefund(RefundableProcessor processor, String transactionId) { return processor.refund(transactionId); }
public static void main(String[] args) { // Usage - Type-safe and correct! CreditCardProcessor ccProcessor = new CreditCardProcessor(); CryptocurrencyProcessor cryptoProcessor = new CryptocurrencyProcessor();
// Both can process payments processAnyPayment(ccProcessor, 100); // ✅ Works processAnyPayment(cryptoProcessor, 100); // ✅ Works
// Only refundable processors can refund processRefund(ccProcessor, "CC-100"); // ✅ Works // processRefund(cryptoProcessor, "CRYPTO-100"); // ✅ Compile error - prevents bugs! }}Why this follows LSP:
- Each class honors its specific contract
CryptocurrencyProcessordoesn’t promise refunds it can’t deliver- Type system prevents using non-refundable processors where refunds are needed
- No unexpected behavior or exceptions
Common LSP Violations to Avoid
Section titled “Common LSP Violations to Avoid”1. Throwing New Exceptions
Section titled “1. Throwing New Exceptions”class BaseClass: def process(self, data: str): """Base class doesn't throw exceptions""" return data.upper()
class DerivedClass(BaseClass): def process(self, data: str): """❌ Violates LSP - throws exception base class doesn't throw""" if not data: raise ValueError("Data cannot be empty") # ❌ New exception! return data.upper()public class BaseClass { // Base class doesn't throw exceptions public String process(String data) { return data.toUpperCase(); }}
public class DerivedClass extends BaseClass { // ❌ Violates LSP - throws exception base class doesn't throw @Override public String process(String data) { if (data == null || data.isEmpty()) { throw new IllegalArgumentException("Data cannot be empty"); // ❌ New exception! } return data.toUpperCase(); }}2. Returning Incompatible Types
Section titled “2. Returning Incompatible Types”class BaseClass: def get_value(self) -> int: """Returns integer""" return 42
class DerivedClass(BaseClass): def get_value(self) -> str: """❌ Violates LSP - returns different type""" return "42" # ❌ Should return int, not str!public class BaseClass { // Returns integer public int getValue() { return 42; }}
public class DerivedClass extends BaseClass { // ❌ Violates LSP - returns different type // This won't compile in Java - return type must be compatible // @Override // public String getValue() { // Compile error! // return "42"; // ❌ Should return int, not String! // }}3. Weakening Preconditions
Section titled “3. Weakening Preconditions”class BaseClass: def process(self, value: int): """Accepts any integer""" if value < 0: raise ValueError("Value must be positive") return value * 2
class DerivedClass(BaseClass): def process(self, value: int): """❌ Violates LSP - requires value > 10 (stronger precondition)""" if value <= 10: # ❌ Weaker precondition - should accept any positive! raise ValueError("Value must be greater than 10") return value * 2public class BaseClass { // Accepts any integer public int process(int value) { if (value < 0) { throw new IllegalArgumentException("Value must be positive"); } return value * 2; }}
public class DerivedClass extends BaseClass { // ❌ Violates LSP - requires value > 10 (stronger precondition) @Override public int process(int value) { if (value <= 10) { // ❌ Weaker precondition - should accept any positive! throw new IllegalArgumentException("Value must be greater than 10"); } return value * 2; }}4. Strengthening Postconditions
Section titled “4. Strengthening Postconditions”class BaseClass: def calculate(self, x: int) -> int: """Returns any integer""" return x * 2
class DerivedClass(BaseClass): def calculate(self, x: int) -> int: """❌ Violates LSP - only returns even numbers (stronger postcondition)""" result = x * 2 if result % 2 != 0: # ❌ Should accept any result! raise ValueError("Result must be even") return resultpublic class BaseClass { // Returns any integer public int calculate(int x) { return x * 2; }}
public class DerivedClass extends BaseClass { // ❌ Violates LSP - only returns even numbers (stronger postcondition) @Override public int calculate(int x) { int result = x * 2; if (result % 2 != 0) { // ❌ Should accept any result! throw new IllegalArgumentException("Result must be even"); } return result; }}Benefits of Following LSP
Section titled “Benefits of Following LSP”Key Takeaways
Section titled “Key Takeaways”Remember: The Liskov Substitution Principle ensures that inheritance is used correctly and polymorphism works as expected! 🎯