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 }}class Rectangle { protected width: number; protected height: number;
constructor(width: number, height: number) { this.width = width; this.height = height; }
// Setting width should only change width setWidth(width: number): void { this.width = width; }
// Setting height should only change height setHeight(height: number): void { this.height = height; }
getArea(): number { return this.width * this.height; }}
// ❌ This violates LSP!class Square extends Rectangle { private side: number;
constructor(side: number) { super(side, side); this.side = side; }
// Problem: Setting width also changes height! setWidth(width: number): void { this.width = width; this.height = width; // ❌ This breaks the contract! this.side = width; }
// Problem: Setting height also changes width! setHeight(height: number): void { this.height = height; this.width = height; // ❌ This breaks the contract! this.side = height; }}
// This function expects Rectangle behaviorfunction testRectangle(rect: Rectangle): number { // This function assumes Rectangle's contract const 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();}
// Usage - This breaks!const rectangle = new Rectangle(5, 5);console.log(testRectangle(rectangle)); // Works: 50
const square = new Square(5);console.log(testRectangle(square)); // ❌ Breaks! Returns 25, not 50#include <iostream>
class Rectangle {protected: double width; double height;
public: Rectangle(double width, double height) : width(width), height(height) {}
// Setting width should only change width virtual void setWidth(double width) { this->width = width; }
// Setting height should only change height virtual void setHeight(double height) { this->height = height; }
virtual double getArea() { return width * height; }
virtual ~Rectangle() = default;};
// ❌ This violates LSP!class Square : public Rectangle {private: double side;
public: Square(double side) : Rectangle(side, side), side(side) {}
// Problem: Setting width also changes height! void setWidth(double width) override { this->width = width; this->height = width; // ❌ This breaks the contract! this->side = width; }
// Problem: Setting height also changes width! void setHeight(double height) override { this->height = height; this->width = height; // ❌ This breaks the contract! this->side = height; }};
// This function expects Rectangle behaviordouble testRectangle(Rectangle& rect) { // This function assumes Rectangle's contract 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();}
int main() { // Usage - This breaks! Rectangle rectangle(5, 5); std::cout << testRectangle(rectangle) << std::endl; // Works: 50
Square square(5); std::cout << testRectangle(square) << std::endl; // ❌ Breaks! Returns 25, not 50
return 0;}using System;
public 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 virtual void SetWidth(double width) { this.width = width; }
// Setting height should only change height public virtual void SetHeight(double height) { this.height = height; }
public virtual double GetArea() { return width * height; }}
// ❌ This violates LSP!public class Square : Rectangle{ private double side;
public Square(double side) : base(side, side) { this.side = side; }
// Problem: Setting width also changes height! public override void SetWidth(double width) { this.width = width; this.height = width; // ❌ This breaks the contract! this.side = width; }
// Problem: Setting height also changes width! public override void SetHeight(double height) { this.height = height; this.width = height; // ❌ This breaks the contract! this.side = height; }}
// This function expects Rectangle behaviorclass Program{ // This function assumes Rectangle's contract 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(); }
static void Main() { // Usage - This breaks! Rectangle rectangle = new Rectangle(5, 5); Console.WriteLine(TestRectangle(rectangle)); // Works: 50
Square square = new Square(5); Console.WriteLine(TestRectangle(square)); // ❌ Breaks! Returns 25, not 50 }}package main
import "fmt"
type Resizable interface { SetWidth(float64) SetHeight(float64) GetArea() float64}
type Rectangle struct{ width, height float64 }
func NewRectangle(w, h float64) *Rectangle { return &Rectangle{width: w, height: h} }func (r *Rectangle) SetWidth(w float64) { r.width = w }func (r *Rectangle) SetHeight(h float64) { r.height = h }func (r *Rectangle) GetArea() float64 { return r.width * r.height }
// ❌ Square claims to be Resizable but breaks Rectangle's contracttype Square struct{ side float64 }
func NewSquare(side float64) *Square { return &Square{side: side} }
// Problem: both setters change the same fieldfunc (s *Square) SetWidth(side float64) { s.side = side }func (s *Square) SetHeight(side float64) { s.side = side }func (s *Square) GetArea() float64 { return s.side * s.side }
func testResizable(r Resizable) float64 { r.SetWidth(10) // Should only change width r.SetHeight(5) // Should only change height // Expected area: 50 — but Square gives 25 ❌ return r.GetArea()}
func main() { rect := NewRectangle(5, 5) fmt.Println(testResizable(rect)) // ✅ 50
sq := NewSquare(5) fmt.Println(testResizable(sq)) // ❌ 25, not 50}struct Rectangle { width: f64, height: f64,}struct Square { size: f64,}impl Rectangle { fn set_width(&mut self, width: f64) { self.width = width; } fn set_height(&mut self, height: f64) { self.height = height; } fn area(&self) -> f64 { self.width * self.height }}// Forcing Square into Rectangle setters breaks expectations.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
# Usage - Both work perfectly!rectangle = Rectangle(5, 10)square = Square(5)print(calculate_total_area([rectangle, square])) # ✅ Works: 75import java.util.List;
// 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; }
public static void main(String[] args) { // Usage - Both work perfectly! Rectangle rectangle = new Rectangle(5, 10); Square square = new Square(5); System.out.println(calculateTotalArea(List.of(rectangle, square))); // ✅ Works: 75.0 }}// Base class - defines the contractabstract class Shape { /** Calculate and return the area */ abstract getArea(): number;}
// Rectangle honors Shape's contractclass Rectangle extends Shape { private width: number; private height: number;
constructor(width: number, height: number) { super(); this.width = width; this.height = height; }
// Only changes width - maintains contract setWidth(width: number): void { this.width = width; }
// Only changes height - maintains contract setHeight(height: number): void { this.height = height; }
getArea(): number { return this.width * this.height; }}
// Square honors Shape's contractclass Square extends Shape { private side: number;
constructor(side: number) { super(); this.side = side; }
// Square-specific method setSide(side: number): void { this.side = side; }
getArea(): number { return this.side * this.side; }}
// This function works with any Shapefunction calculateTotalArea(shapes: Shape[]): number { /** Works with any Shape subclass - LSP satisfied! */ let total = 0; for (const shape of shapes) { total += shape.getArea(); // ✅ Both Rectangle and Square work! } return total;}
// Usage - Both work perfectly!const rectangle = new Rectangle(5, 10);const square = new Square(5);console.log(calculateTotalArea([rectangle, square])); // ✅ Works: 75#include <vector>#include <iostream>
// Base class - defines the contractclass Shape {public: virtual ~Shape() = default; // Calculate and return the area virtual double getArea() const = 0;};
// Rectangle honors Shape's contractclass Rectangle : public Shape {private: double width; double height;
public: Rectangle(double width, double height) : width(width), height(height) {}
// Only changes width - maintains contract void setWidth(double width) { this->width = width; }
// Only changes height - maintains contract void setHeight(double height) { this->height = height; }
double getArea() const override { return width * height; }};
// Square honors Shape's contractclass Square : public Shape {private: double side;
public: Square(double side) : side(side) {}
// Square-specific method void setSide(double side) { this->side = side; }
double getArea() const override { return side * side; }};
// This function works with any Shapedouble calculateTotalArea(const std::vector<Shape*>& shapes) { // Works with any Shape subclass - LSP satisfied! double total = 0; for (const auto* shape : shapes) { total += shape->getArea(); // ✅ Both Rectangle and Square work! } return total;}
int main() { // Usage - Both work perfectly! Rectangle rectangle(5, 10); Square square(5);
std::vector<Shape*> shapes = {&rectangle, &square}; std::cout << calculateTotalArea(shapes) << std::endl; // ✅ Works: 75
return 0;}using System;using System.Collections.Generic;using System.Linq;
// Base class - defines the contractpublic abstract class Shape{ // Calculate and return the area public abstract double GetArea();}
// Rectangle honors Shape's contractpublic class Rectangle : 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; }
public override double GetArea() { return width * height; }}
// Square honors Shape's contractpublic class Square : Shape{ private double side;
public Square(double side) { this.side = side; }
// Square-specific method public void SetSide(double side) { this.side = side; }
public override 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; foreach (Shape shape in shapes) { total += shape.GetArea(); // ✅ Both Rectangle and Square work! } return total; }}
class Program{ static void Main() { // Usage - Both work perfectly! Rectangle rectangle = new Rectangle(5, 10); Square square = new Square(5);
Console.WriteLine(ShapeCalculator.CalculateTotalArea( new List<Shape> { rectangle, square })); // ✅ Works: 75 }}package main
import "fmt"
// Base interface - defines the contracttype Shape interface { GetArea() float64}
// Rectangle honors the Shape contracttype Rectangle struct{ width, height float64 }
func NewRectangle(w, h float64) *Rectangle { return &Rectangle{width: w, height: h} }func (r *Rectangle) SetWidth(w float64) { r.width = w }func (r *Rectangle) SetHeight(h float64) { r.height = h }func (r *Rectangle) GetArea() float64 { return r.width * r.height }
// Square honors the Shape contracttype Square struct{ side float64 }
func NewSquare(side float64) *Square { return &Square{side: side} }func (s *Square) SetSide(side float64) { s.side = side }func (s *Square) GetArea() float64 { return s.side * s.side }
func calculateTotalArea(shapes []Shape) float64 { total := 0.0 for _, s := range shapes { total += s.GetArea() // ✅ Both Rectangle and Square work! } return total}
func main() { rect := NewRectangle(5, 10) sq := NewSquare(5) fmt.Println(calculateTotalArea([]Shape{rect, sq})) // ✅ 75}trait Shape { fn area(&self) -> f64;}struct Rectangle { width: f64, height: f64,}struct Square { size: f64,}impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height }}impl Shape for Square { fn area(&self) -> f64 { self.size * self.size }}fn print_area(shape: &dyn Shape) { println!("Area: {}", shape.area());}Why this follows LSP:
- Both
RectangleandSquarehonor theShapecontract - They can be used interchangeably where
Shapeis expected - No unexpected behavior changes
Example 2: Payment Processing System
Section titled “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)
Section titled “Violating LSP (Bad Approach)”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 }}// Base payment processorabstract class PaymentProcessor { // Process payment and return transaction ID abstract processPayment(amount: number): string;
// Refund a transaction - should always work refund(transactionId: string): boolean { console.log(`Refunding transaction ${transactionId}`); return true; }}
// Credit card payments - follows contractclass CreditCardProcessor extends PaymentProcessor { processPayment(amount: number): string { const transactionId = `CC-${amount}`; console.log(`Processing credit card payment of $${amount}`); // Process credit card... return transactionId; }
refund(transactionId: string): boolean { console.log(`Refunding credit card transaction ${transactionId}`); // Refund credit card... return true; }}
// ❌ Cryptocurrency payments - violates LSP!class CryptocurrencyProcessor extends PaymentProcessor { processPayment(amount: number): string { const transactionId = `CRYPTO-${amount}`; console.log(`Processing cryptocurrency payment of $${amount}`); // Process crypto... return transactionId; }
// ❌ Problem: Cryptocurrency refunds might not be possible! refund(transactionId: string): boolean { throw new Error("Cryptocurrency refunds are not supported!"); // This breaks the contract - refund() should always work }}
// This function expects PaymentProcessor's contractfunction processRefund(processor: PaymentProcessor, transactionId: string): void { // This function assumes refund() always works try { const success = processor.refund(transactionId); if (success) { console.log("Refund successful!"); } else { console.log("Refund failed!"); } } catch (e) { console.log(`Unexpected error: ${e}`); // ❌ Breaks when using CryptocurrencyProcessor! }}
// Usage - This breaks!const ccProcessor = new CreditCardProcessor();const cryptoProcessor = new CryptocurrencyProcessor();
processRefund(ccProcessor, "CC-100"); // ✅ WorksprocessRefund(cryptoProcessor, "CRYPTO-100"); // ❌ Breaks! Raises exception#include <string>#include <iostream>#include <stdexcept>
// Base payment processorclass PaymentProcessor {public: virtual ~PaymentProcessor() = default;
// Process payment and return transaction ID virtual std::string processPayment(double amount) = 0;
// Refund a transaction - should always work virtual bool refund(const std::string& transactionId) { std::cout << "Refunding transaction " << transactionId << std::endl; return true; }};
// Credit card payments - follows contractclass CreditCardProcessor : public PaymentProcessor {public: std::string processPayment(double amount) override { std::string transactionId = "CC-" + std::to_string(amount); std::cout << "Processing credit card payment of $" << amount << std::endl; // Process credit card... return transactionId; }
bool refund(const std::string& transactionId) override { std::cout << "Refunding credit card transaction " << transactionId << std::endl; // Refund credit card... return true; }};
// ❌ Cryptocurrency payments - violates LSP!class CryptocurrencyProcessor : public PaymentProcessor {public: std::string processPayment(double amount) override { std::string transactionId = "CRYPTO-" + std::to_string(amount); std::cout << "Processing cryptocurrency payment of $" << amount << std::endl; // Process crypto... return transactionId; }
// ❌ Problem: Cryptocurrency refunds might not be possible! bool refund(const std::string& transactionId) override { throw std::runtime_error("Cryptocurrency refunds are not supported!"); // This breaks the contract - refund() should always work }};
// This function expects PaymentProcessor's contractvoid processRefund(PaymentProcessor& processor, const std::string& transactionId) { // This function assumes refund() always works try { bool success = processor.refund(transactionId); if (success) { std::cout << "Refund successful!" << std::endl; } else { std::cout << "Refund failed!" << std::endl; } } catch (const std::exception& e) { std::cout << "Unexpected error: " << e.what() << std::endl; // ❌ Breaks when using CryptocurrencyProcessor! }}
int main() { // Usage - This breaks! CreditCardProcessor ccProcessor; CryptocurrencyProcessor cryptoProcessor;
processRefund(ccProcessor, "CC-100"); // ✅ Works processRefund(cryptoProcessor, "CRYPTO-100"); // ❌ Breaks! Throws exception
return 0;}using System;
// 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 virtual bool Refund(string transactionId) { Console.WriteLine($"Refunding transaction {transactionId}"); return true; }}
// Credit card payments - follows contractpublic class CreditCardProcessor : PaymentProcessor{ public override string ProcessPayment(double amount) { string transactionId = $"CC-{amount}"; Console.WriteLine($"Processing credit card payment of ${amount}"); // Process credit card... return transactionId; }
public override bool Refund(string transactionId) { Console.WriteLine($"Refunding credit card transaction {transactionId}"); // Refund credit card... return true; }}
// ❌ Cryptocurrency payments - violates LSP!public class CryptocurrencyProcessor : PaymentProcessor{ public override string ProcessPayment(double amount) { string transactionId = $"CRYPTO-{amount}"; Console.WriteLine($"Processing cryptocurrency payment of ${amount}"); // Process crypto... return transactionId; }
// ❌ Problem: Cryptocurrency refunds might not be possible! public override bool Refund(string transactionId) { throw new NotSupportedException("Cryptocurrency refunds are not supported!"); // This breaks the contract - refund() should always work }}
// This function expects PaymentProcessor's contractclass RefundProcessor{ // This function assumes refund() always works public static void ProcessRefund(PaymentProcessor processor, string transactionId) { try { bool success = processor.Refund(transactionId); if (success) { Console.WriteLine("Refund successful!"); } else { Console.WriteLine("Refund failed!"); } } catch (Exception e) { Console.WriteLine($"Unexpected error: {e.Message}"); // ❌ Breaks when using CryptocurrencyProcessor! } }
static void Main() { // Usage - This breaks! CreditCardProcessor ccProcessor = new CreditCardProcessor(); CryptocurrencyProcessor cryptoProcessor = new CryptocurrencyProcessor();
ProcessRefund(ccProcessor, "CC-100"); // ✅ Works ProcessRefund(cryptoProcessor, "CRYPTO-100"); // ❌ Breaks! Throws exception }}package main
import ( "errors" "fmt")
type PaymentProcessor interface { ProcessPayment(amount float64) (string, error) Refund(txID string) (bool, error)}
type CreditCardProcessor struct{}
func (p *CreditCardProcessor) ProcessPayment(amount float64) (string, error) { txID := fmt.Sprintf("CC-%.2f", amount) fmt.Printf("Processing credit card payment of $%.2f\n", amount) return txID, nil}
func (p *CreditCardProcessor) Refund(txID string) (bool, error) { fmt.Printf("Refunding credit card transaction %s\n", txID) return true, nil}
// ❌ Cryptocurrency payments - violates LSP!type CryptocurrencyProcessor struct{}
func (p *CryptocurrencyProcessor) ProcessPayment(amount float64) (string, error) { txID := fmt.Sprintf("CRYPTO-%.2f", amount) fmt.Printf("Processing cryptocurrency payment of $%.2f\n", amount) return txID, nil}
// ❌ Problem: Refund always fails — breaks the contract!func (p *CryptocurrencyProcessor) Refund(txID string) (bool, error) { return false, errors.New("cryptocurrency refunds are not supported")}
func processRefund(processor PaymentProcessor, txID string) { success, err := processor.Refund(txID) if err != nil { fmt.Printf("Unexpected error: %v\n", err) // ❌ Breaks with CryptocurrencyProcessor! return } if success { fmt.Println("Refund successful!") }}
func main() { cc := &CreditCardProcessor{} crypto := &CryptocurrencyProcessor{}
processRefund(cc, "CC-100") // ✅ Works processRefund(crypto, "CRYPTO-100") // ❌ Always errors}trait PaymentProcessor { fn process(&self, amount: f64) -> Result<String, String>;}struct CreditCardProcessor;struct CashOnDeliveryProcessor;impl PaymentProcessor for CreditCardProcessor { fn process(&self, amount: f64) -> Result<String, String> { Ok(format!("Charged {:.2}", amount)) }}impl PaymentProcessor for CashOnDeliveryProcessor { fn process(&self, _amount: f64) -> Result<String, String> { Err("Cannot process immediately".to_string()) }}// The implementation weakens the promised behavior.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! }}// Base class - all payments can be processedabstract class PaymentProcessor { /** Process payment and return transaction ID */ abstract processPayment(amount: number): string;}
// Interface for processors that support refundsabstract class RefundableProcessor extends PaymentProcessor { /** Refund a transaction - only if processor supports it */ abstract refund(transactionId: string): boolean;}
// Credit card - supports both payment and refundclass CreditCardProcessor extends RefundableProcessor { processPayment(amount: number): string { const transactionId = `CC-${amount}`; console.log(`Processing credit card payment of $${amount}`); return transactionId; }
refund(transactionId: string): boolean { console.log(`Refunding credit card transaction ${transactionId}`); return true; }}
// Cryptocurrency - only supports payment, no refundsclass CryptocurrencyProcessor extends PaymentProcessor { processPayment(amount: number): string { const transactionId = `CRYPTO-${amount}`; console.log(`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 contractsfunction processAnyPayment(processor: PaymentProcessor, amount: number): string { /** Works with any PaymentProcessor */ return processor.processPayment(amount);}
function processRefund(processor: RefundableProcessor, transactionId: string): boolean { /** Only works with RefundableProcessor - type-safe! */ return processor.refund(transactionId);}
// Usage - Type-safe and correct!const ccProcessor = new CreditCardProcessor();const cryptoProcessor = new CryptocurrencyProcessor();
// Both can process paymentsprocessAnyPayment(ccProcessor, 100); // ✅ WorksprocessAnyPayment(cryptoProcessor, 100); // ✅ Works
// Only refundable processors can refundprocessRefund(ccProcessor, "CC-100"); // ✅ Works// processRefund(cryptoProcessor, "CRYPTO-100"); // ✅ Type error - prevents bugs!#include <string>#include <iostream>
// Base class - all payments can be processedclass PaymentProcessor {public: virtual ~PaymentProcessor() = default; // Process payment and return transaction ID virtual std::string processPayment(double amount) = 0;};
// Interface for processors that support refundsclass RefundableProcessor : public PaymentProcessor {public: // Refund a transaction - only if processor supports it virtual bool refund(const std::string& transactionId) = 0;};
// Credit card - supports both payment and refundclass CreditCardProcessor : public RefundableProcessor {public: std::string processPayment(double amount) override { std::string transactionId = "CC-" + std::to_string(amount); std::cout << "Processing credit card payment of $" << amount << std::endl; return transactionId; }
bool refund(const std::string& transactionId) override { std::cout << "Refunding credit card transaction " << transactionId << std::endl; return true; }};
// Cryptocurrency - only supports payment, no refundsclass CryptocurrencyProcessor : public PaymentProcessor {public: std::string processPayment(double amount) override { std::string transactionId = "CRYPTO-" + std::to_string(amount); std::cout << "Processing cryptocurrency payment of $" << amount << std::endl; return transactionId; } // No refund() method - this is correct! Don't implement what you can't support};
// Functions that work with specific contractsstd::string processAnyPayment(PaymentProcessor& processor, double amount) { // Works with any PaymentProcessor return processor.processPayment(amount);}
bool processRefundFunc(RefundableProcessor& processor, const std::string& transactionId) { // Only works with RefundableProcessor - type-safe! return processor.refund(transactionId);}
int main() { // Usage - Type-safe and correct! CreditCardProcessor ccProcessor; CryptocurrencyProcessor cryptoProcessor;
// Both can process payments processAnyPayment(ccProcessor, 100); // ✅ Works processAnyPayment(cryptoProcessor, 100); // ✅ Works
// Only refundable processors can refund processRefundFunc(ccProcessor, "CC-100"); // ✅ Works // processRefundFunc(cryptoProcessor, "CRYPTO-100"); // ✅ Compile error - prevents bugs!
return 0;}using System;
// 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 : PaymentProcessor{ // Refund a transaction - only if processor supports it public abstract bool Refund(string transactionId);}
// Credit card - supports both payment and refundpublic class CreditCardProcessor : RefundableProcessor{ public override string ProcessPayment(double amount) { string transactionId = $"CC-{amount}"; Console.WriteLine($"Processing credit card payment of ${amount}"); return transactionId; }
public override bool Refund(string transactionId) { Console.WriteLine($"Refunding credit card transaction {transactionId}"); return true; }}
// Cryptocurrency - only supports payment, no refundspublic class CryptocurrencyProcessor : PaymentProcessor{ public override string ProcessPayment(double amount) { string transactionId = $"CRYPTO-{amount}"; Console.WriteLine($"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 bool ProcessRefund(RefundableProcessor processor, string transactionId) { return processor.Refund(transactionId); }}
class Program{ static void Main() { // Usage - Type-safe and correct! CreditCardProcessor ccProcessor = new CreditCardProcessor(); CryptocurrencyProcessor cryptoProcessor = new CryptocurrencyProcessor();
// Both can process payments PaymentService.ProcessAnyPayment(ccProcessor, 100); // ✅ Works PaymentService.ProcessAnyPayment(cryptoProcessor, 100); // ✅ Works
// Only refundable processors can refund PaymentService.ProcessRefund(ccProcessor, "CC-100"); // ✅ Works // PaymentService.ProcessRefund(cryptoProcessor, "CRYPTO-100"); // ✅ Compile error - prevents bugs! }}package main
import "fmt"
// Base interface - all payments can be processedtype PaymentProcessor interface { ProcessPayment(amount float64) (string, error)}
// Extended interface - only for processors that support refundstype RefundableProcessor interface { PaymentProcessor Refund(txID string) (bool, error)}
type CreditCardProcessor struct{}
func (p *CreditCardProcessor) ProcessPayment(amount float64) (string, error) { txID := fmt.Sprintf("CC-%.2f", amount) fmt.Printf("Processing credit card payment of $%.2f\n", amount) return txID, nil}
func (p *CreditCardProcessor) Refund(txID string) (bool, error) { fmt.Printf("Refunding credit card transaction %s\n", txID) return true, nil}
// Cryptocurrency - only supports payment, no refundstype CryptocurrencyProcessor struct{}
func (p *CryptocurrencyProcessor) ProcessPayment(amount float64) (string, error) { txID := fmt.Sprintf("CRYPTO-%.2f", amount) fmt.Printf("Processing cryptocurrency payment of $%.2f\n", amount) return txID, nil}// No Refund() method — correct! Don't implement what you can't support.
func processAnyPayment(p PaymentProcessor, amount float64) (string, error) { return p.ProcessPayment(amount)}
func processRefund(p RefundableProcessor, txID string) (bool, error) { return p.Refund(txID)}
func main() { cc := &CreditCardProcessor{} crypto := &CryptocurrencyProcessor{}
processAnyPayment(cc, 100) // ✅ Works processAnyPayment(crypto, 100) // ✅ Works
processRefund(cc, "CC-100") // ✅ Works // processRefund(crypto, "CRYPTO-100") // ✅ Compile error — CryptocurrencyProcessor doesn't satisfy RefundableProcessor}trait PaymentProcessor { fn process(&self, amount: f64) -> String;}struct CreditCardProcessor;struct WalletProcessor;impl PaymentProcessor for CreditCardProcessor { fn process(&self, amount: f64) -> String { format!("Card charged {:.2}", amount) }}impl PaymentProcessor for WalletProcessor { fn process(&self, amount: f64) -> String { format!("Wallet charged {:.2}", amount) }}fn checkout(processor: &dyn PaymentProcessor, amount: f64) { println!("{}", processor.process(amount));}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(); }}class BaseClass { // Base class doesn't throw exceptions process(data: string): string { return data.toUpperCase(); }}
class DerivedClass extends BaseClass { // ❌ Violates LSP - throws exception base class doesn't throw process(data: string): string { if (!data) { throw new Error("Data cannot be empty"); // ❌ New exception! } return data.toUpperCase(); }}#include <string>#include <stdexcept>#include <algorithm>
class BaseClass {public: virtual ~BaseClass() = default;
// Base class doesn't throw exceptions virtual std::string process(const std::string& data) { std::string result = data; std::transform(result.begin(), result.end(), result.begin(), ::toupper); return result; }};
class DerivedClass : public BaseClass {public: // ❌ Violates LSP - throws exception base class doesn't throw std::string process(const std::string& data) override { if (data.empty()) { throw std::invalid_argument("Data cannot be empty"); // ❌ New exception! } std::string result = data; std::transform(result.begin(), result.end(), result.begin(), ::toupper); return result; }};using System;
public class BaseClass{ // Base class doesn't throw exceptions public virtual string Process(string data) { return data.ToUpper(); }}
public class DerivedClass : BaseClass{ // ❌ Violates LSP - throws exception base class doesn't throw public override string Process(string data) { if (string.IsNullOrEmpty(data)) { throw new ArgumentException("Data cannot be empty"); // ❌ New exception! } return data.ToUpper(); }}package main
import ( "errors" "strings")
type BaseProcessor struct{}
func (b *BaseProcessor) Process(data string) (string, error) { return strings.ToUpper(data), nil}
type DerivedProcessor struct{}
// ❌ Violates LSP - returns error that base never returnsfunc (d *DerivedProcessor) Process(data string) (string, error) { if data == "" { return "", errors.New("data cannot be empty") // ❌ New error type! } return strings.ToUpper(data), nil}trait Repository { fn find_user(&self, id: i32) -> Result<String, String>;}struct CacheRepository;impl Repository for CacheRepository { fn find_user(&self, id: i32) -> Result<String, String> { if id <= 0 { return Err("cache-specific error".to_string()); } Ok("Alice".to_string()) }}// Surprise errors make callers care about the concrete type.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! // }}class BaseClass { // Returns number getValue(): number { return 42; }}
class DerivedClass extends BaseClass { // ❌ Violates LSP - returns different type // This won't work in TypeScript - return type must be compatible // getValue(): string { // Type error! // return "42"; // ❌ Should return number, not string! // }
// TypeScript enforces return type compatibility getValue(): number { return 42; // Must match base class return type }}#include <string>
class BaseClass {public: virtual ~BaseClass() = default;
// Returns integer virtual int getValue() { return 42; }};
class DerivedClass : public BaseClass {public: // ❌ Violates LSP - returns different type // This won't compile in C++ - return type must match exactly // std::string getValue() override { // Compile error! // return "42"; // ❌ Should return int, not string! // }
// C++ enforces return type compatibility int getValue() override { return 42; // Must match base class return type }};using System;
public class BaseClass{ // Returns integer public virtual int GetValue() { return 42; }}
public class DerivedClass : BaseClass{ // ❌ Violates LSP - returns different type // This won't compile in C# - return type must be compatible // public override string GetValue() // Compile error! // { // return "42"; // ❌ Should return int, not string! // }
// C# enforces return type compatibility public override int GetValue() { return 42; // Must match base class return type }}package main
// Go's interface enforces return type at compile time.
type Getter interface { GetValue() int // contract: returns int}
type BaseClass struct{}
func (b *BaseClass) GetValue() int { return 42 }
// ✅ Go enforces return type compatibilitytype DerivedClass struct{}
func (d *DerivedClass) GetValue() int { return 42 // Must match the interface — returning string is a compile error}
// The following would NOT compile:// func (d *DerivedClass) GetValue() string { return "42" } // ❌ Does not satisfy Gettertrait UserRepository { fn find_user(&self, id: i32) -> Option<String>;}struct BrokenRepository;impl BrokenRepository { fn find_user_raw(&self, _id: i32) -> Vec<String> { vec!["Alice".to_string()] }}// Returning a different shape breaks the expected contract.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; }}class BaseClass { // Accepts any integer process(value: number): number { if (value < 0) { throw new Error("Value must be positive"); } return value * 2; }}
class DerivedClass extends BaseClass { // ❌ Violates LSP - requires value > 10 (stronger precondition) process(value: number): number { if (value <= 10) { // ❌ Stronger precondition - should accept any positive! throw new Error("Value must be greater than 10"); } return value * 2; }}#include <stdexcept>
class BaseClass {public: virtual ~BaseClass() = default;
// Accepts any integer virtual int process(int value) { if (value < 0) { throw std::invalid_argument("Value must be positive"); } return value * 2; }};
class DerivedClass : public BaseClass {public: // ❌ Violates LSP - requires value > 10 (stronger precondition) int process(int value) override { if (value <= 10) { // ❌ Stronger precondition - should accept any positive! throw std::invalid_argument("Value must be greater than 10"); } return value * 2; }};using System;
public class BaseClass{ // Accepts any integer public virtual int Process(int value) { if (value < 0) { throw new ArgumentException("Value must be positive"); } return value * 2; }}
public class DerivedClass : BaseClass{ // ❌ Violates LSP - requires value > 10 (stronger precondition) public override int Process(int value) { if (value <= 10) // ❌ Stronger precondition - should accept any positive! { throw new ArgumentException("Value must be greater than 10"); } return value * 2; }}package main
import "errors"
type BaseProcessor struct{}
func (b *BaseProcessor) Process(value int) (int, error) { if value < 0 { return 0, errors.New("value must be positive") } return value * 2, nil}
type DerivedProcessor struct{}
// ❌ Violates LSP - stronger precondition than basefunc (d *DerivedProcessor) Process(value int) (int, error) { if value <= 10 { // ❌ Stronger precondition - should accept any positive! return 0, errors.New("value must be greater than 10") } return value * 2, nil}trait Discount { fn apply(&self, amount: f64) -> f64;}struct StrictDiscount;impl Discount for StrictDiscount { fn apply(&self, amount: f64) -> f64 { assert!(amount >= 100.0, "minimum purchase required"); amount * 0.9 }}// The implementation requires more than the trait promised.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; }}class BaseClass { // Returns any number calculate(x: number): number { return x * 2; }}
class DerivedClass extends BaseClass { // ❌ Violates LSP - only returns even numbers (stronger postcondition) calculate(x: number): number { const result = x * 2; if (result % 2 !== 0) { // ❌ Should accept any result! throw new Error("Result must be even"); } return result; }}#include <stdexcept>
class BaseClass {public: virtual ~BaseClass() = default;
// Returns any integer virtual int calculate(int x) { return x * 2; }};
class DerivedClass : public BaseClass {public: // ❌ Violates LSP - only returns even numbers (stronger postcondition) int calculate(int x) override { int result = x * 2; if (result % 2 != 0) { // ❌ Should accept any result! throw std::invalid_argument("Result must be even"); } return result; }};using System;
public class BaseClass{ // Returns any integer public virtual int Calculate(int x) { return x * 2; }}
public class DerivedClass : BaseClass{ // ❌ Violates LSP - only returns even numbers (stronger postcondition) public override int Calculate(int x) { int result = x * 2; if (result % 2 != 0) // ❌ Should accept any result! { throw new ArgumentException("Result must be even"); } return result; }}package main
import "errors"
type BaseCalc struct{}
func (b *BaseCalc) Calculate(x int) (int, error) { return x * 2, nil}
type DerivedCalc struct{}
// ❌ Violates LSP - stronger postcondition than basefunc (d *DerivedCalc) Calculate(x int) (int, error) { result := x * 2 if result%2 != 0 { // ❌ Should accept any result! return 0, errors.New("result must be even") } return result, nil}trait Account { fn withdraw(&mut self, amount: f64) -> f64;}struct FeeAccount { balance: f64,}impl Account for FeeAccount { fn withdraw(&mut self, amount: f64) -> f64 { self.balance -= amount + 5.0; amount - 5.0 }}// The caller expects the requested amount, but receives less.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! 🎯