TechWayFit
● Design Principles

Liskov Substitution Principle - Building Reliable Inheritance Hierarchies

Tech Buddy August 11, 2025 15 min read
Liskov Substitution Principle - Building Reliable Inheritance Hierarchies

🔄 Understanding the Liskov Substitution Principle

The Liskov Substitution Principle (LSP) is the third principle in the SOLID design principles, formulated by Barbara Liskov in 1987. It states that objects of a superclass should be replaceable with objects of its subclasses without breaking the application.

In simpler terms, if class B is a subtype of class A, then we should be able to replace A with B without disrupting the behavior of our program. This means that subclasses should extend the functionality of their parent class without changing or removing the parent's behavior.

When LSP is violated, the consequences can be significant:

  • Unexpected Behavior: Substituting a derived class for its base class leads to unexpected results, making the code unpredictable and unreliable.
  • Broken Polymorphism: The core benefit of polymorphism is lost when derived classes don't properly implement the interface contract of their base class.
  • Complex Client Code: Clients need to know about specific subclass implementations and handle them differently, violating the abstraction principle.
  • Runtime Errors: Type checking and casting become necessary, leading to potential runtime exceptions and making the code more error-prone.
  • Testing Difficulties: Each subclass requires different testing approaches since they can't be treated uniformly, increasing test complexity.
  • Maintenance Overhead: Changes to base classes or interfaces require careful analysis of all derived classes to ensure LSP compliance.

By adhering to LSP, you ensure that your inheritance hierarchies are well-designed, your polymorphic code works correctly, and your abstractions remain meaningful and reliable.

💻 Examples of LSP in Action

Let's explore how LSP can be applied in real-world scenarios using multiple programming languages. These examples demonstrate common violations and how to refactor them for proper substitutability.

Basic Example: Rectangle and Square

// ❌ Violates LSP - Square changes behavior of Rectangle
                      public class Rectangle {
                          public virtual double Width { get; set; }
                          public virtual double Height { get; set; }
                          
                          public double CalculateArea() {
                              return Width * Height;
                          }
                      }
                      
                      public class Square : Rectangle {
                          public override double Width {
                              get => base.Width;
                              set {
                                  base.Width = value;
                                  base.Height = value; // Unexpected side effect!
                              }
                          }
                          
                          public override double Height {
                              get => base.Height;
                              set {
                                  base.Width = value;  // Unexpected side effect!
                                  base.Height = value;
                              }
                          }
                      }
                      
                      // This violates LSP because:
                      // Rectangle rect = new Square();
                      // rect.Width = 5;
                      // rect.Height = 10;
                      // Console.WriteLine(rect.CalculateArea()); // Expected: 50, Actual: 100
                      
                      // ✅ Follows LSP - Use composition or redesign the hierarchy
                      public abstract class Shape {
                          public abstract double CalculateArea();
                      }
                      
                      public class Rectangle : Shape {
                          public double Width { get; set; }
                          public double Height { get; set; }
                          
                          public override double CalculateArea() {
                              return Width * Height;
                          }
                      }
                      
                      public class Square : Shape {
                          public double Side { get; set; }
                          
                          public override double CalculateArea() {
                              return Side * Side;
                          }
                      }
                      
                      // Alternative approach using immutable objects
                      public abstract class ImmutableShape {
                          public abstract double CalculateArea();
                      }
                      
                      public class ImmutableRectangle : ImmutableShape {
                          public double Width { get; }
                          public double Height { get; }
                          
                          public ImmutableRectangle(double width, double height) {
                              Width = width;
                              Height = height;
                          }
                          
                          public override double CalculateArea() {
                              return Width * Height;
                          }
                      }
                      
                      public class ImmutableSquare : ImmutableShape {
                          public double Side { get; }
                          
                          public ImmutableSquare(double side) {
                              Side = side;
                          }
                          
                          public override double CalculateArea() {
                              return Side * Side;
                          }
                      }
                        

Python Example: Bird Hierarchy

# ❌ Violates LSP - Not all birds can fly
                      class Bird:
                          def fly(self):
                              print("Flying through the sky")
                          
                          def make_sound(self):
                              print("Generic bird sound")
                      
                      class Duck(Bird):
                          def fly(self):
                              print("Duck flying")
                          
                          def make_sound(self):
                              print("Quack!")
                      
                      class Ostrich(Bird):
                          def fly(self):
                              raise NotImplementedError("Ostriches cannot fly!")  # LSP violation!
                          
                          def make_sound(self):
                              print("Ostrich sound")
                      
                      # This breaks when we try to use polymorphism:
                      # birds = [Duck(), Ostrich()]
                      # for bird in birds:
                      #     bird.fly()  # This will crash when it reaches Ostrich!
                      
                      # ✅ Follows LSP - Redesign hierarchy based on capabilities
                      from abc import ABC, abstractmethod
                      
                      class Bird(ABC):
                          @abstractmethod
                          def make_sound(self):
                              pass
                          
                          @abstractmethod
                          def move(self):
                              pass
                      
                      class FlyingBird(Bird):
                          @abstractmethod
                          def fly(self):
                              pass
                          
                          def move(self):
                              self.fly()
                      
                      class FlightlessBird(Bird):
                          @abstractmethod
                          def walk(self):
                              pass
                          
                          def move(self):
                              self.walk()
                      
                      class Duck(FlyingBird):
                          def fly(self):
                              print("Duck flying through the air")
                          
                          def make_sound(self):
                              print("Quack!")
                      
                      class Eagle(FlyingBird):
                          def fly(self):
                              print("Eagle soaring high")
                          
                          def make_sound(self):
                              print("Screech!")
                      
                      class Ostrich(FlightlessBird):
                          def walk(self):
                              print("Ostrich running fast")
                          
                          def make_sound(self):
                              print("Deep ostrich call")
                      
                      class Penguin(FlightlessBird):
                          def walk(self):
                              print("Penguin waddling")
                          
                          def swim(self):
                              print("Penguin swimming gracefully")
                          
                          def make_sound(self):
                              print("Penguin chirp")
                          
                          def move(self):
                              # Penguins can both walk and swim
                              print("Penguin moving: ", end="")
                              self.walk()
                      
                      # Now we can use polymorphism safely:
                      # flying_birds = [Duck(), Eagle()]
                      # for bird in flying_birds:
                      #     bird.fly()  # Safe - all flying birds can fly
                      #
                      # all_birds = [Duck(), Eagle(), Ostrich(), Penguin()]
                      # for bird in all_birds:
                      #     bird.make_sound()  # Safe - all birds make sounds
                      #     bird.move()        # Safe - all birds can move in their own way
                        

JavaScript Example: Payment Processing

// ❌ Violates LSP - Different error handling behaviors
                      class PaymentProcessor {
                          processPayment(amount) {
                              if (amount <= 0) {
                                  throw new Error('Invalid amount');
                              }
                              console.log(`Processing payment of $${amount}`);
                              return { success: true, transactionId: Date.now() };
                          }
                          
                          refundPayment(transactionId) {
                              console.log(`Refunding transaction ${transactionId}`);
                              return { success: true };
                          }
                      }
                      
                      class CreditCardProcessor extends PaymentProcessor {
                          processPayment(amount) {
                              if (amount <= 0) {
                                  throw new Error('Invalid amount');
                              }
                              if (amount > 10000) {
                                  throw new Error('Amount exceeds credit limit');
                              }
                              console.log(`Processing credit card payment of $${amount}`);
                              return { success: true, transactionId: `CC_${Date.now()}` };
                          }
                      }
                      
                      class GiftCardProcessor extends PaymentProcessor {
                          constructor(balance) {
                              super();
                              this.balance = balance;
                          }
                          
                          processPayment(amount) {
                              if (amount <= 0) {
                                  throw new Error('Invalid amount');
                              }
                              if (amount > this.balance) {
                                  // LSP violation - returns failure instead of throwing like parent
                                  return { success: false, error: 'Insufficient balance' };
                              }
                              this.balance -= amount;
                              console.log(`Processing gift card payment of $${amount}`);
                              return { success: true, transactionId: `GC_${Date.now()}` };
                          }
                          
                          refundPayment(transactionId) {
                              // LSP violation - doesn't actually process refunds
                              console.log(`Gift cards cannot be refunded`);
                              return { success: false, error: 'Refunds not supported' };
                          }
                      }
                      
                      // ✅ Follows LSP - Consistent behavior and error handling
                      class PaymentResult {
                          constructor(success, transactionId = null, error = null) {
                              this.success = success;
                              this.transactionId = transactionId;
                              this.error = error;
                          }
                      }
                      
                      class BasePaymentProcessor {
                          processPayment(amount) {
                              if (amount <= 0) {
                                  return new PaymentResult(false, null, 'Invalid amount');
                              }
                              return this.doProcessPayment(amount);
                          }
                          
                          doProcessPayment(amount) {
                              throw new Error('doProcessPayment must be implemented by subclass');
                          }
                          
                          refundPayment(transactionId) {
                              if (!transactionId) {
                                  return new PaymentResult(false, null, 'Invalid transaction ID');
                              }
                              return this.doRefundPayment(transactionId);
                          }
                          
                          doRefundPayment(transactionId) {
                              throw new Error('doRefundPayment must be implemented by subclass');
                          }
                          
                          canProcessAmount(amount) {
                              return true; // Default implementation
                          }
                          
                          supportsRefunds() {
                              return true; // Default implementation
                          }
                      }
                      
                      class ImprovedCreditCardProcessor extends BasePaymentProcessor {
                          doProcessPayment(amount) {
                              if (amount > 10000) {
                                  return new PaymentResult(false, null, 'Amount exceeds credit limit');
                              }
                              console.log(`Processing credit card payment of $${amount}`);
                              return new PaymentResult(true, `CC_${Date.now()}`);
                          }
                          
                          doRefundPayment(transactionId) {
                              console.log(`Refunding credit card transaction ${transactionId}`);
                              return new PaymentResult(true, `REF_${Date.now()}`);
                          }
                          
                          canProcessAmount(amount) {
                              return amount <= 10000;
                          }
                      }
                      
                      class ImprovedGiftCardProcessor extends BasePaymentProcessor {
                          constructor(balance) {
                              super();
                              this.balance = balance;
                          }
                          
                          doProcessPayment(amount) {
                              if (amount > this.balance) {
                                  return new PaymentResult(false, null, 'Insufficient balance');
                              }
                              this.balance -= amount;
                              console.log(`Processing gift card payment of $${amount}`);
                              return new PaymentResult(true, `GC_${Date.now()}`);
                          }
                          
                          doRefundPayment(transactionId) {
                              return new PaymentResult(false, null, 'Gift card refunds not supported');
                          }
                          
                          canProcessAmount(amount) {
                              return amount <= this.balance;
                          }
                          
                          supportsRefunds() {
                              return false;
                          }
                      }
                      
                      // Now all processors can be used interchangeably:
                      function processPayments(processors, amount) {
                          processors.forEach(processor => {
                              if (processor.canProcessAmount(amount)) {
                                  const result = processor.processPayment(amount);
                                  console.log(`Payment result:`, result);
                              } else {
                                  console.log(`Processor cannot handle amount: $${amount}`);
                              }
                          });
                      }
                      
                      const processors = [
                          new ImprovedCreditCardProcessor(),
                          new ImprovedGiftCardProcessor(500)
                      ];
                      
                      processPayments(processors, 100); // Works consistently for all processors
                        

Java Example: File Operations

// ❌ Violates LSP - ReadOnlyFile changes expected behavior
                      public abstract class FileOperations {
                          protected String fileName;
                          
                          public FileOperations(String fileName) {
                              this.fileName = fileName;
                          }
                          
                          public abstract String read();
                          public abstract void write(String content);
                          public abstract void delete();
                      }
                      
                      public class RegularFile extends FileOperations {
                          public RegularFile(String fileName) {
                              super(fileName);
                          }
                          
                          public String read() {
                              System.out.println("Reading from " + fileName);
                              return "file content";
                          }
                          
                          public void write(String content) {
                              System.out.println("Writing to " + fileName + ": " + content);
                          }
                          
                          public void delete() {
                              System.out.println("Deleting " + fileName);
                          }
                      }
                      
                      public class ReadOnlyFile extends FileOperations {
                          public ReadOnlyFile(String fileName) {
                              super(fileName);
                          }
                          
                          public String read() {
                              System.out.println("Reading from read-only " + fileName);
                              return "read-only content";
                          }
                          
                          public void write(String content) {
                              // LSP violation - throws exception instead of performing operation
                              throw new UnsupportedOperationException("Cannot write to read-only file");
                          }
                          
                          public void delete() {
                              // LSP violation - throws exception instead of performing operation
                              throw new UnsupportedOperationException("Cannot delete read-only file");
                          }
                      }
                      
                      // ✅ Follows LSP - Redesign hierarchy based on capabilities
                      public interface Readable {
                          String read();
                      }
                      
                      public interface Writable {
                          void write(String content);
                      }
                      
                      public interface Deletable {
                          void delete();
                      }
                      
                      public abstract class BaseFile implements Readable {
                          protected String fileName;
                          
                          public BaseFile(String fileName) {
                              this.fileName = fileName;
                          }
                          
                          public String getFileName() {
                              return fileName;
                          }
                      }
                      
                      public class RegularFile extends BaseFile implements Writable, Deletable {
                          public RegularFile(String fileName) {
                              super(fileName);
                          }
                          
                          @Override
                          public String read() {
                              System.out.println("Reading from " + fileName);
                              return "file content";
                          }
                          
                          @Override
                          public void write(String content) {
                              System.out.println("Writing to " + fileName + ": " + content);
                          }
                          
                          @Override
                          public void delete() {
                              System.out.println("Deleting " + fileName);
                          }
                      }
                      
                      public class ReadOnlyFile extends BaseFile {
                          public ReadOnlyFile(String fileName) {
                              super(fileName);
                          }
                          
                          @Override
                          public String read() {
                              System.out.println("Reading from read-only " + fileName);
                              return "read-only content";
                          }
                      }
                      
                      public class SystemFile extends BaseFile implements Writable {
                          public SystemFile(String fileName) {
                              super(fileName);
                          }
                          
                          @Override
                          public String read() {
                              System.out.println("Reading system file " + fileName);
                              return "system file content";
                          }
                          
                          @Override
                          public void write(String content) {
                              System.out.println("Writing to system file " + fileName + ": " + content);
                          }
                          // Note: SystemFile is not Deletable - system files cannot be deleted
                      }
                      
                      // Usage with proper abstraction
                      public class FileManager {
                          public void readFiles(List files) {
                              for (Readable file : files) {
                                  String content = file.read(); // Safe - all Readable files can be read
                                  System.out.println("Content: " + content);
                              }
                          }
                          
                          public void writeToFiles(List files, String content) {
                              for (Writable file : files) {
                                  file.write(content); // Safe - all Writable files can be written to
                              }
                          }
                          
                          public void deleteFiles(List files) {
                              for (Deletable file : files) {
                                  file.delete(); // Safe - all Deletable files can be deleted
                              }
                          }
                      }
                        

🌍 Real-World LSP Examples

Let's examine complex, real-world scenarios where LSP compliance is crucial for maintainable and reliable software systems.

Example 1: Database Connection Pool

// ❌ Violates LSP - Different connection types have inconsistent behavior
                      public abstract class DatabaseConnection {
                          public abstract void Connect();
                          public abstract void Disconnect();
                          public abstract void ExecuteQuery(string sql);
                          public abstract void BeginTransaction();
                          public abstract void CommitTransaction();
                          public abstract void RollbackTransaction();
                      }
                      
                      public class SqlServerConnection : DatabaseConnection {
                          public override void Connect() {
                              Console.WriteLine("Connecting to SQL Server");
                          }
                          
                          public override void Disconnect() {
                              Console.WriteLine("Disconnecting from SQL Server");
                          }
                          
                          public override void ExecuteQuery(string sql) {
                              Console.WriteLine($"Executing SQL Server query: {sql}");
                          }
                          
                          public override void BeginTransaction() {
                              Console.WriteLine("Beginning SQL Server transaction");
                          }
                          
                          public override void CommitTransaction() {
                              Console.WriteLine("Committing SQL Server transaction");
                          }
                          
                          public override void RollbackTransaction() {
                              Console.WriteLine("Rolling back SQL Server transaction");
                          }
                      }
                      
                      public class NoSqlConnection : DatabaseConnection {
                          public override void Connect() {
                              Console.WriteLine("Connecting to NoSQL database");
                          }
                          
                          public override void Disconnect() {
                              Console.WriteLine("Disconnecting from NoSQL database");
                          }
                          
                          public override void ExecuteQuery(string sql) {
                              // LSP violation - NoSQL doesn't use SQL!
                              throw new NotSupportedException("NoSQL doesn't support SQL queries");
                          }
                          
                          public override void BeginTransaction() {
                              // LSP violation - Many NoSQL databases don't support transactions
                              throw new NotSupportedException("NoSQL doesn't support transactions");
                          }
                          
                          public override void CommitTransaction() {
                              throw new NotSupportedException("NoSQL doesn't support transactions");
                          }
                          
                          public override void RollbackTransaction() {
                              throw new NotSupportedException("NoSQL doesn't support transactions");
                          }
                      }
                      
                      // ✅ Follows LSP - Design based on actual capabilities
                      public interface IConnection {
                          void Connect();
                          void Disconnect();
                          bool IsConnected { get; }
                      }
                      
                      public interface IQueryable {
                          TResult ExecuteQuery(IQuery query);
                      }
                      
                      public interface ITransactional {
                          ITransaction BeginTransaction();
                      }
                      
                      public interface IQuery {
                          TResult Execute(IConnection connection);
                      }
                      
                      public interface ITransaction : IDisposable {
                          void Commit();
                          void Rollback();
                          bool IsActive { get; }
                      }
                      
                      public abstract class BaseConnection : IConnection {
                          protected bool _isConnected;
                          
                          public bool IsConnected => _isConnected;
                          
                          public abstract void Connect();
                          public abstract void Disconnect();
                      }
                      
                      public class SqlServerConnection : BaseConnection, IQueryable, ITransactional {
                          private ITransaction _currentTransaction;
                          
                          public override void Connect() {
                              Console.WriteLine("Connecting to SQL Server");
                              _isConnected = true;
                          }
                          
                          public override void Disconnect() {
                              Console.WriteLine("Disconnecting from SQL Server");
                              _isConnected = false;
                          }
                          
                          public TResult ExecuteQuery(IQuery query) {
                              if (!IsConnected) {
                                  throw new InvalidOperationException("Not connected to database");
                              }
                              return query.Execute(this);
                          }
                          
                          public ITransaction BeginTransaction() {
                              if (!IsConnected) {
                                  throw new InvalidOperationException("Not connected to database");
                              }
                              _currentTransaction = new SqlTransaction();
                              Console.WriteLine("Beginning SQL Server transaction");
                              return _currentTransaction;
                          }
                      }
                      
                      public class NoSqlConnection : BaseConnection, IQueryable {
                          public override void Connect() {
                              Console.WriteLine("Connecting to NoSQL database");
                              _isConnected = true;
                          }
                          
                          public override void Disconnect() {
                              Console.WriteLine("Disconnecting from NoSQL database");
                              _isConnected = false;
                          }
                          
                          public TResult ExecuteQuery(IQuery query) {
                              if (!IsConnected) {
                                  throw new InvalidOperationException("Not connected to database");
                              }
                              return query.Execute(this);
                          }
                          // Note: NoSqlConnection doesn't implement ITransactional because it doesn't support transactions
                      }
                      
                      public class InMemoryConnection : BaseConnection, IQueryable, ITransactional {
                          private readonly Dictionary _data = new();
                          private ITransaction _currentTransaction;
                          
                          public override void Connect() {
                              Console.WriteLine("Connecting to in-memory database");
                              _isConnected = true;
                          }
                          
                          public override void Disconnect() {
                              Console.WriteLine("Disconnecting from in-memory database");
                              _isConnected = false;
                          }
                          
                          public TResult ExecuteQuery(IQuery query) {
                              if (!IsConnected) {
                                  throw new InvalidOperationException("Not connected to database");
                              }
                              return query.Execute(this);
                          }
                          
                          public ITransaction BeginTransaction() {
                              if (!IsConnected) {
                                  throw new InvalidOperationException("Not connected to database");
                              }
                              _currentTransaction = new InMemoryTransaction();
                              Console.WriteLine("Beginning in-memory transaction");
                              return _currentTransaction;
                          }
                      }
                      
                      // Usage that respects LSP
                      public class DatabaseService {
                          public void ProcessConnections(List connections) {
                              foreach (var connection in connections) {
                                  connection.Connect(); // Safe - all connections can connect
                                  Console.WriteLine($"Connection status: {connection.IsConnected}");
                                  connection.Disconnect(); // Safe - all connections can disconnect
                              }
                          }
                          
                          public void ExecuteQueries(List queryableConnections, IQuery query) {
                              foreach (var connection in queryableConnections) {
                                  var result = connection.ExecuteQuery(query); // Safe - all queryable connections support queries
                                  Console.WriteLine($"Query result: {result}");
                              }
                          }
                          
                          public void PerformTransactions(List transactionalConnections) {
                              foreach (var connection in transactionalConnections) {
                                  using (var transaction = connection.BeginTransaction()) {
                                      // Perform transaction operations
                                      transaction.Commit(); // Safe - all transactional connections support transactions
                                  }
                              }
                          }
                      }
                        

Example 2: Media Player System

# ❌ Violates LSP - Different media types have incompatible interfaces
                      class MediaPlayer:
                          def play(self):
                              raise NotImplementedError
                          
                          def pause(self):
                              raise NotImplementedError
                          
                          def stop(self):
                              raise NotImplementedError
                          
                          def seek(self, position):
                              raise NotImplementedError
                          
                          def get_duration(self):
                              raise NotImplementedError
                      
                      class AudioPlayer(MediaPlayer):
                          def __init__(self, file_path):
                              self.file_path = file_path
                              self.is_playing = False
                              self.position = 0
                              self.duration = 180  # 3 minutes
                          
                          def play(self):
                              print(f"Playing audio: {self.file_path}")
                              self.is_playing = True
                          
                          def pause(self):
                              print("Pausing audio")
                              self.is_playing = False
                          
                          def stop(self):
                              print("Stopping audio")
                              self.is_playing = False
                              self.position = 0
                          
                          def seek(self, position):
                              if 0 <= position <= self.duration:
                                  self.position = position
                                  print(f"Seeking to {position} seconds")
                              else:
                                  raise ValueError("Invalid position")
                          
                          def get_duration(self):
                              return self.duration
                      
                      class LiveStreamPlayer(MediaPlayer):
                          def __init__(self, stream_url):
                              self.stream_url = stream_url
                              self.is_playing = False
                          
                          def play(self):
                              print(f"Starting live stream: {self.stream_url}")
                              self.is_playing = True
                          
                          def pause(self):
                              print("Pausing live stream")
                              self.is_playing = False
                          
                          def stop(self):
                              print("Stopping live stream")
                              self.is_playing = False
                          
                          def seek(self, position):
                              # LSP violation - live streams can't seek!
                              raise NotSupportedError("Cannot seek in live streams")
                          
                          def get_duration(self):
                              # LSP violation - live streams don't have duration!
                              raise NotSupportedError("Live streams don't have duration")
                      
                      # ✅ Follows LSP - Design based on actual capabilities
                      from abc import ABC, abstractmethod
                      from typing import Optional
                      
                      class Playable(ABC):
                          @abstractmethod
                          def play(self):
                              pass
                          
                          @abstractmethod
                          def stop(self):
                              pass
                          
                          @abstractmethod
                          def is_playing(self) -> bool:
                              pass
                      
                      class Pausable(ABC):
                          @abstractmethod
                          def pause(self):
                              pass
                          
                          @abstractmethod
                          def is_paused(self) -> bool:
                              pass
                      
                      class Seekable(ABC):
                          @abstractmethod
                          def seek(self, position: int):
                              pass
                          
                          @abstractmethod
                          def get_position(self) -> int:
                              pass
                      
                      class HasDuration(ABC):
                          @abstractmethod
                          def get_duration(self) -> int:
                              pass
                      
                      class VolumeControl(ABC):
                          @abstractmethod
                          def set_volume(self, volume: float):
                              pass
                          
                          @abstractmethod
                          def get_volume(self) -> float:
                              pass
                      
                      class BaseMediaPlayer(Playable):
                          def __init__(self, source: str):
                              self.source = source
                              self._is_playing = False
                          
                          def is_playing(self) -> bool:
                              return self._is_playing
                      
                      class AudioFilePlayer(BaseMediaPlayer, Pausable, Seekable, HasDuration, VolumeControl):
                          def __init__(self, file_path: str):
                              super().__init__(file_path)
                              self._is_paused = False
                              self._position = 0
                              self._duration = 180  # 3 minutes
                              self._volume = 1.0
                          
                          def play(self):
                              print(f"Playing audio file: {self.source}")
                              self._is_playing = True
                              self._is_paused = False
                          
                          def stop(self):
                              print("Stopping audio file")
                              self._is_playing = False
                              self._is_paused = False
                              self._position = 0
                          
                          def pause(self):
                              if self._is_playing:
                                  print("Pausing audio file")
                                  self._is_paused = True
                                  self._is_playing = False
                          
                          def is_paused(self) -> bool:
                              return self._is_paused
                          
                          def seek(self, position: int):
                              if 0 <= position <= self._duration:
                                  self._position = position
                                  print(f"Seeking to {position} seconds")
                              else:
                                  raise ValueError("Invalid seek position")
                          
                          def get_position(self) -> int:
                              return self._position
                          
                          def get_duration(self) -> int:
                              return self._duration
                          
                          def set_volume(self, volume: float):
                              if 0.0 <= volume <= 1.0:
                                  self._volume = volume
                                  print(f"Volume set to {volume * 100}%")
                              else:
                                  raise ValueError("Volume must be between 0.0 and 1.0")
                          
                          def get_volume(self) -> float:
                              return self._volume
                      
                      class LiveStreamPlayer(BaseMediaPlayer, Pausable, VolumeControl):
                          def __init__(self, stream_url: str):
                              super().__init__(stream_url)
                              self._is_paused = False
                              self._volume = 1.0
                          
                          def play(self):
                              print(f"Starting live stream: {self.source}")
                              self._is_playing = True
                              self._is_paused = False
                          
                          def stop(self):
                              print("Stopping live stream")
                              self._is_playing = False
                              self._is_paused = False
                          
                          def pause(self):
                              if self._is_playing:
                                  print("Pausing live stream")
                                  self._is_paused = True
                                  self._is_playing = False
                          
                          def is_paused(self) -> bool:
                              return self._is_paused
                          
                          def set_volume(self, volume: float):
                              if 0.0 <= volume <= 1.0:
                                  self._volume = volume
                                  print(f"Stream volume set to {volume * 100}%")
                              else:
                                  raise ValueError("Volume must be between 0.0 and 1.0")
                          
                          def get_volume(self) -> float:
                              return self._volume
                      
                      class RadioPlayer(BaseMediaPlayer, VolumeControl):
                          def __init__(self, frequency: str):
                              super().__init__(frequency)
                              self._volume = 1.0
                          
                          def play(self):
                              print(f"Tuning to radio frequency: {self.source}")
                              self._is_playing = True
                          
                          def stop(self):
                              print("Stopping radio")
                              self._is_playing = False
                          
                          def set_volume(self, volume: float):
                              if 0.0 <= volume <= 1.0:
                                  self._volume = volume
                                  print(f"Radio volume set to {volume * 100}%")
                              else:
                                  raise ValueError("Volume must be between 0.0 and 1.0")
                          
                          def get_volume(self) -> float:
                              return self._volume
                      
                      # Usage that respects LSP
                      class MediaController:
                          def play_all(self, players: list[Playable]):
                              """Play all playable media - works with any Playable implementation"""
                              for player in players:
                                  player.play()
                          
                          def pause_all(self, players: list[Pausable]):
                              """Pause all pausable media - only works with pausable players"""
                              for player in players:
                                  if player.is_playing():
                                      player.pause()
                          
                          def seek_all_to_position(self, players: list[Seekable], position: int):
                              """Seek all seekable media - only works with seekable players"""
                              for player in players:
                                  try:
                                      player.seek(position)
                                  except ValueError as e:
                                      print(f"Seek failed: {e}")
                          
                          def show_durations(self, players: list[HasDuration]):
                              """Show duration for all media with duration - only works with players that have duration"""
                              for player in players:
                                  duration = player.get_duration()
                                  print(f"Duration: {duration} seconds")
                          
                          def set_volume_for_all(self, players: list[VolumeControl], volume: float):
                              """Set volume for all players with volume control"""
                              for player in players:
                                  player.set_volume(volume)
                      
                      # Example usage
                      audio_player = AudioFilePlayer("song.mp3")
                      stream_player = LiveStreamPlayer("http://stream.example.com")
                      radio_player = RadioPlayer("101.5 FM")
                      
                      controller = MediaController()
                      
                      # All players can be played
                      all_players = [audio_player, stream_player, radio_player]
                      controller.play_all(all_players)
                      
                      # Only some players can be paused
                      pausable_players = [audio_player, stream_player]  # radio can't be paused
                      controller.pause_all(pausable_players)
                      
                      # Only audio files can be seeked
                      seekable_players = [audio_player]
                      controller.seek_all_to_position(seekable_players, 30)
                      
                      # Only audio files have duration
                      duration_players = [audio_player]
                      controller.show_durations(duration_players)
                      
                      # All players have volume control
                      controller.set_volume_for_all(all_players, 0.8)
                        

Benefits of LSP-Compliant Design

  • Predictable Behavior: Subclasses behave consistently with their parent classes, making the code more reliable and easier to understand.
  • True Polymorphism: Objects can be used interchangeably through their common interfaces without unexpected behavior.
  • Simplified Client Code: Clients can work with abstractions without needing to know about specific implementations.
  • Better Testing: All implementations of an interface can be tested using the same test suite, ensuring consistent behavior.
  • Easier Maintenance: Changes to interfaces are automatically enforced across all implementations, reducing the risk of breaking changes.
  • Enhanced Reusability: Components can be reused in different contexts because they adhere to well-defined contracts.

🎯 LSP Implementation Guidelines

Here are key guidelines and patterns for implementing the Liskov Substitution Principle effectively in your code.

1. Contract Compliance

Ensure that subclasses honor the contracts established by their parent classes or interfaces.

// ✅ Good: Subclass strengthens postconditions, weakens preconditions
                      public abstract class NumberValidator {
                          // Precondition: number can be any double
                          // Postcondition: returns true if valid, false otherwise
                          public abstract bool IsValid(double number);
                      }
                      
                      public class PositiveNumberValidator : NumberValidator {
                          // Precondition: still accepts any double (not strengthened)
                          // Postcondition: returns true only for positive numbers (stronger than parent)
                          public override bool IsValid(double number) {
                              return number > 0;
                          }
                      }
                      
                      public class NonZeroNumberValidator : NumberValidator {
                          // Precondition: still accepts any double (not strengthened)
                          // Postcondition: returns true for any non-zero number (stronger than parent)
                          public override bool IsValid(double number) {
                              return number != 0;
                          }
                      }
                      
                      // ❌ Bad: Subclass strengthens preconditions
                      public class RestrictiveValidator : NumberValidator {
                          public override bool IsValid(double number) {
                              // Strengthens precondition - now only accepts numbers between 1 and 100
                              if (number < 1 || number > 100) {
                                  throw new ArgumentException("Number must be between 1 and 100");
                              }
                              return true;
                          }
                      }
                        

2. Exception Handling Consistency

Subclasses should not throw new exceptions that are not expected by the parent class contract.

# ✅ Good: Consistent exception handling
                      class DataProcessor:
                          def process(self, data: str) -> str:
                              """Process data. Raises ValueError for invalid data."""
                              if not data:
                                  raise ValueError("Data cannot be empty")
                              return data.upper()
                      
                      class JsonProcessor(DataProcessor):
                          def process(self, data: str) -> str:
                              """Process JSON data. Raises ValueError for invalid data."""
                              if not data:
                                  raise ValueError("Data cannot be empty")
                              
                              import json
                              try:
                                  # Parse to validate JSON
                                  json.loads(data)
                                  return data.upper()
                              except json.JSONDecodeError:
                                  # Still raises ValueError as expected by parent contract
                                  raise ValueError("Invalid JSON format")
                      
                      class XmlProcessor(DataProcessor):
                          def process(self, data: str) -> str:
                              """Process XML data. Raises ValueError for invalid data."""
                              if not data:
                                  raise ValueError("Data cannot be empty")
                              
                              import xml.etree.ElementTree as ET
                              try:
                                  # Parse to validate XML
                                  ET.fromstring(data)
                                  return data.upper()
                              except ET.ParseError:
                                  # Still raises ValueError as expected by parent contract
                                  raise ValueError("Invalid XML format")
                      
                      # ❌ Bad: Introduces unexpected exceptions
                      class ProblematicProcessor(DataProcessor):
                          def process(self, data: str) -> str:
                              if not data:
                                  raise ValueError("Data cannot be empty")
                              
                              # LSP violation - introduces new exception type not in parent contract
                              if len(data) > 1000:
                                  raise RuntimeError("Data too large")  # Unexpected exception!
                              
                              return data.upper()
                        

3. Interface Segregation with LSP

Design interfaces based on client needs and ensure all implementations can fulfill the complete interface contract.

// ✅ Good: Well-segregated interfaces that support LSP
                      class Drawable {
                          draw() {
                              throw new Error('draw method must be implemented');
                          }
                      }
                      
                      class Resizable {
                          resize(width, height) {
                              throw new Error('resize method must be implemented');
                          }
                          
                          getWidth() {
                              throw new Error('getWidth method must be implemented');
                          }
                          
                          getHeight() {
                              throw new Error('getHeight method must be implemented');
                          }
                      }
                      
                      class Colorable {
                          setColor(color) {
                              throw new Error('setColor method must be implemented');
                          }
                          
                          getColor() {
                              throw new Error('getColor method must be implemented');
                          }
                      }
                      
                      class Circle extends Drawable {
                          constructor(radius) {
                              super();
                              this.radius = radius;
                          }
                          
                          draw() {
                              console.log(`Drawing circle with radius ${this.radius}`);
                          }
                      }
                      
                      class ColoredCircle extends Circle {
                          constructor(radius, color) {
                              super(radius);
                              this.color = color;
                          }
                          
                          draw() {
                              console.log(`Drawing ${this.color} circle with radius ${this.radius}`);
                          }
                      }
                      
                      // Multiple inheritance through mixins
                      class ResizableColoredRectangle extends Drawable {
                          constructor(width, height, color) {
                              super();
                              this.width = width;
                              this.height = height;
                              this.color = color;
                          }
                          
                          draw() {
                              console.log(`Drawing ${this.color} rectangle ${this.width}x${this.height}`);
                          }
                          
                          // Resizable methods
                          resize(width, height) {
                              this.width = width;
                              this.height = height;
                          }
                          
                          getWidth() {
                              return this.width;
                          }
                          
                          getHeight() {
                              return this.height;
                          }
                          
                          // Colorable methods
                          setColor(color) {
                              this.color = color;
                          }
                          
                          getColor() {
                              return this.color;
                          }
                      }
                      
                      // Usage that respects LSP
                      class ShapeRenderer {
                          renderShapes(shapes) {
                              // All shapes implement Drawable, so this is safe
                              shapes.forEach(shape => shape.draw());
                          }
                          
                          resizeShapes(resizableShapes, newWidth, newHeight) {
                              // Only works with shapes that implement Resizable
                              resizableShapes.forEach(shape => {
                                  if (typeof shape.resize === 'function') {
                                      shape.resize(newWidth, newHeight);
                                  }
                              });
                          }
                          
                          changeColors(colorableShapes, newColor) {
                              // Only works with shapes that implement Colorable
                              colorableShapes.forEach(shape => {
                                  if (typeof shape.setColor === 'function') {
                                      shape.setColor(newColor);
                                  }
                              });
                          }
                      }
                        

4. Design by Contract

Use explicit contracts to ensure LSP compliance through preconditions, postconditions, and invariants.

// ✅ Good: Explicit contracts ensure LSP compliance
                      public interface BankAccount {
                          /**
                           * Withdraws money from the account.
                           * 
                           * Precondition: amount > 0 && amount <= getBalance()
                           * Postcondition: getBalance() == old(getBalance()) - amount
                           * 
                           * @param amount The amount to withdraw
                           * @throws IllegalArgumentException if amount <= 0
                           * @throws InsufficientFundsException if amount > getBalance()
                           */
                          void withdraw(double amount) throws InsufficientFundsException;
                          
                          /**
                           * Deposits money into the account.
                           * 
                           * Precondition: amount > 0
                           * Postcondition: getBalance() == old(getBalance()) + amount
                           * 
                           * @param amount The amount to deposit
                           * @throws IllegalArgumentException if amount <= 0
                           */
                          void deposit(double amount);
                          
                          /**
                           * Gets the current balance.
                           * 
                           * Postcondition: return value >= 0
                           * 
                           * @return The current balance
                           */
                          double getBalance();
                      }
                      
                      public class SavingsAccount implements BankAccount {
                          private double balance;
                          
                          public SavingsAccount(double initialBalance) {
                              if (initialBalance < 0) {
                                  throw new IllegalArgumentException("Initial balance cannot be negative");
                              }
                              this.balance = initialBalance;
                          }
                          
                          @Override
                          public void withdraw(double amount) throws InsufficientFundsException {
                              // Enforce preconditions
                              if (amount <= 0) {
                                  throw new IllegalArgumentException("Amount must be positive");
                              }
                              if (amount > balance) {
                                  throw new InsufficientFundsException("Insufficient funds");
                              }
                              
                              // Perform operation
                              balance -= amount;
                              
                              // Postcondition is automatically satisfied
                          }
                          
                          @Override
                          public void deposit(double amount) {
                              // Enforce precondition
                              if (amount <= 0) {
                                  throw new IllegalArgumentException("Amount must be positive");
                              }
                              
                              // Perform operation
                              balance += amount;
                              
                              // Postcondition is automatically satisfied
                          }
                          
                          @Override
                          public double getBalance() {
                              return balance; // Postcondition: always >= 0
                          }
                      }
                      
                      public class CheckingAccount implements BankAccount {
                          private double balance;
                          private final double overdraftLimit;
                          
                          public CheckingAccount(double initialBalance, double overdraftLimit) {
                              if (initialBalance < 0) {
                                  throw new IllegalArgumentException("Initial balance cannot be negative");
                              }
                              if (overdraftLimit < 0) {
                                  throw new IllegalArgumentException("Overdraft limit cannot be negative");
                              }
                              this.balance = initialBalance;
                              this.overdraftLimit = overdraftLimit;
                          }
                          
                          @Override
                          public void withdraw(double amount) throws InsufficientFundsException {
                              // Enforce preconditions (weakened - allows overdraft)
                              if (amount <= 0) {
                                  throw new IllegalArgumentException("Amount must be positive");
                              }
                              if (amount > balance + overdraftLimit) {
                                  throw new InsufficientFundsException("Exceeds overdraft limit");
                              }
                              
                              // Perform operation
                              balance -= amount;
                              
                              // Postcondition still satisfied: balance change is correct
                          }
                          
                          @Override
                          public void deposit(double amount) {
                              // Same implementation as SavingsAccount
                              if (amount <= 0) {
                                  throw new IllegalArgumentException("Amount must be positive");
                              }
                              balance += amount;
                          }
                          
                          @Override
                          public double getBalance() {
                              // May return negative value, but contract allows this for checking accounts
                              return balance;
                          }
                          
                          public double getAvailableBalance() {
                              return balance + overdraftLimit;
                          }
                      }
                      
                      // Usage that works with any BankAccount implementation
                      public class BankingService {
                          public void transferMoney(BankAccount from, BankAccount to, double amount) 
                                  throws InsufficientFundsException {
                              // This works with any BankAccount implementation due to LSP
                              from.withdraw(amount);  // Contract guarantees this behaves consistently
                              to.deposit(amount);     // Contract guarantees this behaves consistently
                          }
                          
                          public void printBalances(List accounts) {
                              for (BankAccount account : accounts) {
                                  // Contract guarantees getBalance() works consistently
                                  System.out.println("Balance: $" + account.getBalance());
                              }
                          }
                      }
                        

Key LSP Guidelines Summary

  • Honor Parent Contracts: Subclasses must fulfill all contracts established by their parent classes.
  • Maintain Behavioral Consistency: The behavior of subclasses should be predictable based on the parent class interface.
  • Don't Strengthen Preconditions: Subclasses should not require stricter inputs than their parent classes.
  • Don't Weaken Postconditions: Subclasses should provide at least the same guarantees as their parent classes.
  • Preserve Invariants: Class invariants must be maintained across the inheritance hierarchy.
  • Handle Exceptions Consistently: Don't introduce unexpected exception types in subclasses.
  • Use Composition When Inheritance Violates LSP: If "is-a" relationship doesn't hold behaviorally, consider "has-a" relationships.

Comments

Loading comments…

Leave a comment