Details of Liskov Substitution Principle Example

In this post, I will cover the details of the Liskov Substitution Principle(LSP) with an example. This is a key principle to validate the object-oriented design of your system. Hopefully, you will be able to use this in your design and find out if there are any violations. You can learn more about other object oriented design principles. Let’s start with the basics of Liskov Substitution Principle first.

Liskov Substitution Principle (LSP)

Basically, the principle states that if in an object-oriented program you substitute superclass object reference with any of its subclass objects, it should not break the program.

Wikipedia definition says – If S is a subtype of T, then the objects of type T may be replaced with objects of S without altering any of the desirable properties of the program.

LSP comes into play when you have super-sub class OR interface implementation type of inheritance. Usually, when you define a superclass or an interface, it is a contract. Any inherited object from this superclass or interface implementation class must follow the contract. Any of the objects that fail to follow the contract, will violate the Liskov Substitution Principle. If you want to learn more about Object-Oriented Design, get this course from educative.

Let’s take a simple look before we look at this in detail.


public class Bird
{
    void fly()
    {
       // Fly function for bird
    }
}

public class Parrot extends Bird
{
    @Override
    void fly()
    {

    }
}

public class Ostrich extends Bird
{
   // can't implement fly since Ostrich doesn't fly
}

If you look at the above classes, Ostrich is not a bird. Technically, we can still implement the fly method in Ostrich class, but it will be without implementation or throwing some exception. In this case, Ostrich is violating LSP.

Object-Oriented Design can violate the LSP in the following circumstances:

  1. If a subclass returns an object that is completely different from what the superclass returns.
  2. If a subclass throws an exception that is not defined in the superclass.
  3. There are any side effects in subclass methods that were not part of the superclass definition.

How do programmers break this principle?

Sometimes, if a programmer ends up extending a superclass without completely following the contract of the superclass, a programmer will have to use instanceofcheck for the new subclass. If there are more similar subclasses are added to the code, it can increase the complexity of the code and violate the LSP.

Supertype abstract intends to help programmers, but instead, it can end up hindering and add more bugs in the code. That’s why it is important for programmers to be careful when creating a new subclass of a superclass.

Liskov Substitution Principle Example

Now, let’s look at an example in detail.

Many banks offer a basic account as well as a premium account. They also charge fees for these premium accounts while basic accounts are free. So, we will have an abstract class to represent BankAccount.

public abstract class BankAccount
{
   public boolean withDraw(double amount);

   public void deposit(double amount);

}

The class BankAccount has two methods withDraw and deposit.

Consequently, let’s create a class that represents a basic account.


public class BasicAccount extends BankAccount
{
    private double balance;

    @Override
    public boolean withDraw(double amount)
    {
       if(balance > 0)
       {
           balance -= amount;
           if(balance < 0)
           {
              return false;
           }
           else 
           {
              return true;
           }
       }
       else
       {
          return false;
       } 
    }

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

Now, a premium account is a little different. Of course, an account holder will still be able to deposit or withdraw from that account. But with every transaction, the account holder also earns rewards points.


public class PremiumAccount extends BankAccount
{
   private double balance;
   private double rewardPoints;

   @Override
   public boolean withDraw(double amount)
   {
      if(balance > 0)
       {
           balance -= amount;
           if(balance < 0)
           {
              return false;
           }
           else 
           {
              return true;
              updateRewardsPoints();
           }
       }
       else
       {
          return false;
       } 
   }
   
   @Override
   public void deposit(double amount)
   {
      this.balance += amount;
      updateRewardsPoints();
   }

   public void updateRewardsPoints()
   {
      this.rewardsPoints++;
   }
}

So far so good. Everything looks ok. If you want to use the same class of BankAccount to create a new investment account that an account holder can’t withdraw from, it will look like below:


public class InvestmentAccount extends BankAccount
{
   private double balance;

   @Override
   public boolean withDraw(double amount)
   {
      throw new Expcetion("Not supported");
   }
   
   @Override
   public void deposit(double amount)
   {
      this.balance += amount;
   }

}

Even though, this InvestmentAccount follows most of the contract of BankAccount, it does not implement withDraw method and throws an exception that is not in the superclass. In short, this subclass violates the LSP.

How to avoid violating LSP in the design?

So how can we avoid violating LSP in our above example? There are a few ways you can avoid violating Liskov Substitution Principle. First, the superclass should contain the most generic information. We will use some other object-oriented design principles to do design changes in our example to avoid violating LSP.

  • Use Interface instead of Program.
  • Composition over inheritance

So now, we can fix BankAccount class by creating an interface that classes can implement.


public interface IBankAccount
{
  boolean withDraw(double amount) throws InvalidTransactionException;
  void deposit(double amount) throws InvalidTransactionException;
}

Now if we create classes that implement this interface, we can also include a InvestingAccount that won’t implement withDraw method.

If you have used Object-oriented programming, you must have heard both the terms composition and inheritance. Also in object-oriented design, composition over inheritance is a common pattern. One can always make objects more generic. Using composition over inheritance can help to avoid violating LSP.

Combine Object-Oriented Design principles with fundamentals of distributed system design and you will be good at system design.

Conclusion

In this post, we talked about Liskov Substitution Principle and its example, how designers usually violate this principle, and how we can avoid violating it.