Introduction
In this article, we will cover the Liskov Substitution Principle (LSP).
Liskov Substitution Principle (LSP)
- This is a scary term for a very simple concept. It's formally defined as "If S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may substitute objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.)." That's an even scarier definition.
- The best explanation for this is if you have a parent class and a child class, then the base class and child class can be used interchangeably without getting incorrect results. This might still be confusing, so let's take a look at the classic square rectangle example. Mathematically, a square is a rectangle, but if you model it using the "is-a" relationship via inheritance, you quickly get into trouble.
Let’s take another example of Bank Accounts.
Suppose you have two types of bank accounts: a RegularAccount and a FixedTermDepositAccount. The FixedTermDepositAccount doesn’t allow withdrawals until the end of the term.
Violation of LSP/Bad Code
using System;
namespace DesignPattern
{
public class BankAccount
{
protected double balance;
public virtual void Deposit(double amount)
{
balance += amount;
}
public virtual void Withdraw(double amount)
{
if (balance >= amount)
{
balance -= amount;
}
else
{
throw new InvalidOperationException("Insufficient funds");
}
}
public double GetBalance()
{
return balance;
}
}
public class FixedTermDepositAccount : BankAccount
{
public override void Withdraw(double amount)
{
throw new InvalidOperationException("Cannot withdraw from a fixed term deposit account until term ends");
}
}
}
Note. If a FixedTermDepositAccount object is substituted for a BankAccount object in a context that expects withdrawals to be possible, the program will break.
Using LSP / Good Code
using System;
namespace DesignPattern
{
public abstract class BankAccount
{
protected double balance;
public virtual void Deposit(double amount)
{
balance += amount;
Console.WriteLine($"Deposit: {amount}, Total Amount: {balance}");
}
public abstract void Withdraw(double amount);
public double GetBalance()
{
return balance;
}
}
public class RegularAccount : BankAccount
{
public override void Withdraw(double amount)
{
if (balance >= amount)
{
balance -= amount;
Console.WriteLine($"Withdraw: {amount}, Balance: {balance}");
}
else
{
Console.WriteLine($"Trying to Withdraw: {amount}, Insufficient Funds, Available Funds: {balance}");
}
}
}
public class FixedTermDepositAccount : BankAccount
{
private bool termEnded = false; // simplification for the example
public override void Withdraw(double amount)
{
if (!termEnded)
{
Console.WriteLine("Cannot withdraw from a fixed term deposit account until term ends");
}
else if (balance >= amount)
{
balance -= amount;
Console.WriteLine($"Withdraw: {amount}, Balance: {balance}");
}
else
{
Console.WriteLine($"Trying to Withdraw: {amount}, Insufficient Funds, Available Funds: {balance}");
}
}
}
//Testing the Liskov Substitution Principle
public class Program
{
public static void Main()
{
Console.WriteLine("RegularAccount:");
var RegularBankAccount = new RegularAccount();
RegularBankAccount.Deposit(1000);
RegularBankAccount.Deposit(500);
RegularBankAccount.Withdraw(900);
RegularBankAccount.Withdraw(800);
Console.WriteLine("\nFixedTermDepositAccount:");
var FixedTermDepositBankAccount = new FixedTermDepositAccount();
FixedTermDepositBankAccount.Deposit(1000);
FixedTermDepositBankAccount.Withdraw(500);
Console.ReadKey();
}
}
}
When designing the BankAccount class, we make the Withdraw method abstract to enable derived types might have their own rules for withdrawal. This approach helps the client code understand that subclasses may have varying behaviors, and Liskov Substitution Principle (LSP) is not violated. When you run the above code.