SOLID: Liskov Substitution Principle

Posted on

This is a continuation of the SOLID principles series.

The Liskov substitution principle is the most technical principle of all. However, it is the one that most helps to develop decoupled applications, which is the foundation of designing reusable components.

Barbara Liskov defined this principle as follows:

Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T”

The definition given by Liskov is based on the Design by Contract (DbC) defined by Bertrand Meyer. A contract that is identified by preconditions, invariants, and postconditions:

  • A routine can expect a certain condition to be guaranteed on entry by any client module that calls it: the routine’s precondition. This is an obligation for the client and benefit for the supplier, as it frees it from having to handle cases outside of the precondition.
  • A routine can guarantee a certain property on exit: the routine’s postcondition – an obligation for the supplier, and a benefit for the client.
  • Maintain a certain property, assumed on entry and guaranteed on exit: the class invariant.

The concept of contract and implementation is the foundation for inheritance and polymorphism in object-oriented programming.

In 1996, Robert C. Martin redefined the concept given by Liskov, as follows:

Function that use pointers of references to base classes must be able to use objects of derived classes without knowing it.

The redefinition given by Bob Martin helped to simplify the concept implemented by Liskov years before and its adoption by developers.



Violation of the Liskov Substitution Principle

As a developer for a banking entity, you are requested to implement a system for managing bank accounts. Your boss asks you to implement, in the first sprint of the project, a system for managing basic and premium bank accounts. The difference between them is that the latter accumulates preference points on any deposit.

You implement the following abstract class as the foundation of your system.

public abstract class BankAccount {

    /**
     * In charge of depositing a specific amount into the account.
     * @param amount            Dollar ammount.
     */
    public abstract void deposit(double amount);

    /**
     * In charge of withdrawing a specific amount from the account.
     * @param amount            Dollar amount.
     * @return                  Boolean result.
     */
    public abstract boolean withdraw(double amount);
}
Enter fullscreen mode

Exit fullscreen mode

This abstract class defines an obligation for any derived class to override any abstract method defined in the BankAccount class. This means that the basic and premium accounts must override the deposit and withdrawal method.

public class BasicAccount extends BankAccount {

    private double balance;

    @Override
    public void deposit(double amount) {
        this.balance += amount;
    }

    @Override
    public boolean withdraw(double amount) {
        if(this.balance < amount)
            return false;
        else{
            this.balance -= amount;
            return true;
        }       
    }
}
Enter fullscreen mode

Exit fullscreen mode

public class PremiumAccount extends BankAccount {

    private double balance;
    private int preferencePoints;

    @Override
    public void deposit(double amount) {
        this.balance += amount;
        accumulatePreferencePoints();
    }

    @Override
    public boolean withdraw(double amount) {
         if(this.balance < amount)
            return false;
        else{
            this.balance -= amount;
            accumulatePreferencePoints();
            return true;
        }
    }

    private void accumulatePreferencePoints(){
        this.preferencePoints++;
    }

}
Enter fullscreen mode

Exit fullscreen mode

Please take into account that any of these classes have the minimum validations for a production environment.

All basic and premium accounts are discounted by $25.00 annually for administrative expenses. To implement this policy you defined the following class:

public class WithdrawalService {

    public static final double ADMINISTRATIVE_EXPENSES_CHARGE = 25.00;

    public void cargarDebitarCuentas(){

        BankAccount basiAcct = new BasicAccount();
        basiAcct.deposit(100.00);

        BankAccount premiumAcct = new PremiumAccount();
        premiumAcct.deposit(200.00);

        List<BankAccount> accounts = new ArrayList();

        accounts.add(basiAcct);
        accounts.add(premiumAcct);

        debitAdministrativeExpenses(accounts);

    }

    private void debitAdministrativeExpenses(List<BankAccount> accounts){
        accounts.stream()
                .forEach(account -> account.withdraw(WithdrawalService.ADMINISTRATIVE_EXPENSES_CHARGE));
    }
}
Enter fullscreen mode

Exit fullscreen mode

On the second sprint of your project, your boss asks you to implement long-term accounts into your bank account managing system. The differences between long-term accounts and basic/premium accounts are the following:

  • Long-term accounts are exempt from administrative expenses.
  • Long-term accounts don’t allow withdrawals. If the client wants to withdraw any amount of his / her account must be done through a different process.

As a developer in charge of the accounts system, you decide to extend the BankAccount class for the Long-term accounts.

public class LongTermAccount extends BankAccount {

    private double balance;

    @Override
    public void deposit(double amount) {
        this.balance += amount;
    }

    @Override
    public boolean withdraw(double amount) {
        throw new UnsupportedOperationException("Not supported yet."); 
    }
}
Enter fullscreen mode

Exit fullscreen mode

This part is where the violation of the Liskov Substitution Principle is obvious. You cannot extend the BankAccount class in the LongTermAccount without overriding the withdrawal method. However, the long-term accounts don’t allow withdrawals according to your project’s requirements.

You have the following two options to solve this issue:

  • You can override the withdrawal method as an empty method or you can throw an UnsupportedOperationException. However, the BankAccount objects wouldn’t be completely interchangeable with LongTermAccount objects because if we try to execute the withdrawal method we would get an exception. As a solution for this issue, we can condition the debitAdministrativeExpenses method, so we can skip the LongTermAccount objects but this would violate the Open/Closed Principle. For instance:
private void debitAdministrativeExpenses(List<BankAccount> accounts){

        for(BankAccount account : accounts){
            if(account instanceof LongTermAccount)
                continue;
            else
                account.withdraw(ADMINISTRATIVE_EXPENSES_CHARGE);
        }
    }
Enter fullscreen mode

Exit fullscreen mode

  • You can make your code Liskov Substitution Principle compliant.



Implementing Liskov Substitution Principle

The main issue with the bank account structure is that the long-term account is not a regular bank account, at least is not the type defined in the BankAccount abstract class. There is a simple test on the abductive reasoning area that can be used to check if a class is a subtype from “X” type. The duck test states “If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck”. The long-term account looks like a regular bank account but it does not behave like a regular one. To solve this issue we have to change the current class structure.

To make our code LSP compliant, we’ll make the following changes:

  • All types of bank accounts will allow the deposit action.
  • Only the basic and premium bank accounts will allow the withdrawal action.
  • We’ll define an abstract bank account for all types of accounts. This abstract class will define only one method, the deposit method.
  • We’ll extend the BankAccount with WithdrawableAccount abstract class, which will define the debit method.
  • The basic and premium accounts will extend the WithdrawableAccount abstract class, while the long-term account will extend the BankAccount abstract class.

The abstract BankAccount class will define the deposit method.

public abstract class BankAccount {

    /**
     * In charge of depositing a specific amount into the account.
     * @param amount            Dollar ammount.
     */
    public abstract void deposit(double amount);
}
Enter fullscreen mode

Exit fullscreen mode

The abstract WithdrawableAccount class will define the withdrawal method.

public abstract class WithdrawableAccount extends BankAccount {

    /**
     * In charge of withdrawing a specific amount from the account.
     * @param amount            Dollar amount.
     * @return                  Boolean result.
     */
    public abstract boolean withdraw(double amount);
}
Enter fullscreen mode

Exit fullscreen mode

The basic and premium account classes will extend the WithdrawableAccount class, which extends the BankAccount class. This nested inheritance allows the basic/premium accounts to have both methods, deposit, and withdrawal.

public class BasicAccount extends WithdrawableAccount {

    private double balance;

    @Override
    public void deposit(double amount) {
        this.balance += amount;
    }

    @Override
    public boolean withdraw(double monto) {
        if(this.balance < monto)
            return false;
        else{
            this.balance -= monto;
            return true;
        }       
    }   
}
Enter fullscreen mode

Exit fullscreen mode

public class PremiumAccount extends WithdrawableAccount {

    private double balance;
    private int preferencePoints;

    @Override
    public void deposit(double monto) {
        this.balance += monto;
        accumulatePreferencePoints();
    }

    @Override
    public boolean withdraw(double monto) {
         if(this.balance < monto)
            return false;
        else{
            this.balance -= monto;
            accumulatePreferencePoints();
            return true;
        }
    }

    private void accumulatePreferencePoints(){
        this.preferencePoints++;
    }
}
Enter fullscreen mode

Exit fullscreen mode

The WithdrawalService class is implemented using only WithdrawableAccount types or subtypes.

public class WithdrawableService {

public static final double ADMINISTRATIVE_EXPENSES_CHARGE = 25.00;

public void cargarDebitarCuentas(){

WithdrawableAccount basicAcct = new BasicAccount();
basicAcct.deposit(100.00);

WithdrawableAccount premiumAcct = new PremiumAccount();
premiumAcct.deposit(200.00);

List<WithdrawableAccount> accounts = new ArrayList();

accounts.add

Leave a Reply

Your email address will not be published. Required fields are marked *