Liskov substitution principle is one of the SOLID design principles.This principle is related to the derived and base classes.Liskov substitution principle states that the base class objects could be replaced by derived class objects and our program will still work as expected.
So if we have an object x of class B in our program then we can replace x with y where y is an object of class D such that D is derived from B.
To understand this principle we can take an example of a bank account.We can have different types of bank accounts such as savings account and current account.Though there are different types of bank accounts but since all of them have common attributes such as interest rate and operations such as deposit and withdrawal.Hence these account types are considered just as bank accounts.
So we can represent the relationship between these account types as:
public class BankAccount { protected int accountBalance; protected int accountNumber; protected decimal interestRate; public virtual decimal interest { get {return interestRate; } } public BankAccount() { } public BankAccount(int accountNumber) { this.accountNumber = accountNumber; } public virtual void Withdraw(int amount) { accountBalance = accountBalance-amount; } public virtual void Deposit(int amount) { accountBalance = accountBalance+amount; } } public class SavingsAccount:BankAccount { public override decimal interest { get { return 8.00M; } } } public class CurrentAccount : BankAccount { public override decimal interest { get { return 6.00M; } } } }
The above relationship forms an hierarchy which is perfect since the operations have the similar behavior even though they might differ in exact implementation.For example to calculate the interest rate in current account different rate might apply then saving account.But nonetheless these are the different implementations of the same operations.
Now lets say we want add a new class for loan account in the class hierarchy.One way to do it is by deriving a LoanAccount class from the BankAccount class.We can thus implement it as:
public class LoanAccount:BankAccount { public override decimal interest { get { throw new NotImplementedException(); } } public override void Withdraw(int amount) { //can not implement this base class method in this class throw new NotImplementedException(); } public override void Deposit(int amount) { //can not implement this base class method in this class throw new NotImplementedException(); } }
The above relationship might seem to be normal since LoanAccount is also a type of BankAccount.If we observe in detail there is a problem with the above implementation.Though LoanAccount is a type of BankAccount but a loan doesn’t have withdrawal and deposit operations unlike saving and current accounts.That’s why we are not implementing any behavior for deposit and withdraw operations.
This violates the Liskov substitution principle since if we try to pass a reference of LoanAccount type to a method which expects an object of type BankAccount then the method may have undesirable results.In the current implementation if the method calls the Deposit method of the BankAccount then we may get an exception.
public void DepositAmount(BankAccount bankAccount,int amount) { bankAccount.Deposit(amount); //save in database }
We are facing this problem since our solution violates the Liskov substitution principle.
The correct solution to this problem is by deriving a class DepositAccount from the BankAccount class.Our existing classes CurrentAccount and SavingsAccount now derives from this class rather then deriving directly from the BankAccount class.The BankAccount class can contain only the method and properties which are common to all the accounts.
public class BankAccount { protected int accountBalance; protected int accountNumber; protected decimal interestRate; public virtual decimal interest { get {return interestRate; } } public BankAccount() { } public BankAccount(int accountNumber) { this.accountNumber = accountNumber; } } public class DepositAccount:BankAccount { public virtual void Withdraw(int amount) { accountBalance = accountBalance - amount; } public virtual void Deposit(int amount) { accountBalance = accountBalance + amount; } } public class LoanBaseAccount:BankAccount { protected decimal amount { get; set; } public virtual decimal LoanAmount { get { return interestRate; } } } public class SavingsAccount : DepositAccount { public override decimal interest { get { return 8.00M; } } } public class CurrentAccount : DepositAccount { public override decimal interest { get { return 6.00M; } } } public class LoanAccount : LoanBaseAccount { }
As we have segregated the BankAccount class into two separate classses LoanBaseAccount and DepositAccount so the implementing LoanAccount classs need not implement the Withdraw and Deposit methods.
So Liskov substitution principle ensures the correctness of the design and is useful is determining the proper class hierarchies in our program.
Leave a Reply