LSP - Liskov Substitution Principle

Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.

The Liskov Substitution Principle (LSP) applies to inheritance in such a way that the derived classes must be completely substitutable for their base classes. In other words, if class A is a subtype of class B, then we should be able to replace B with A without interrupting the behavior of the program.

Several circumstances may indicate that the LSP has been violated:

  • A subclass changes the behavior of the parent class.

  • A subclass overrides the methods of the parent class but throws an exception.

  • A subclass overrides the methods of the parent class, but with an empty implementation.

  • A subclass inherits methods from the parent class but is not used.

There are two ways to solve this problem:

  • Breaking the hierarchy.

  • Tell, don’t ask.

Example 1 - a subclass changes the behavior of the parent class

To illustrate this, we will go with a classic example of squares and rectangles that people often use to explain LSP because it is very simple and easy to understand.

class Rectangle {
    private int width;
    private int height;
 
    public int calculateArea() {
        return this.width * this.height;
    }
 
    public void setWidth(int width) {
        this.width = width;
    }
 
    public void setHeight(int height) {
        this.height = height;
    }
}
class Square extends Rectangle {
 
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }
 
    @Override
    public void setHeight(int height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}
public class LSPExample1 {

    public void example1() {
        Rectangle rect = new Rectangle();
        rect.setWidth(5);
        rect.setHeight(10);
        System.out.println(rect.calculateArea()); // 50
 
        Square square = new Square();
        square.setWidth(5);
        square.setHeight(10);
        System.out.println(square.calculateArea()); // 100
    }
}

Looking at the example above, we see that all operations are very reasonable. Since a square has 2 sides, every time we set the length of one side, we reset the length of remaining side.

However, the Square class inherits from the Rectangle class but the Square class has different shapes and it changed the behavior of the Rectangle class, resulting in the LSP violation.

As the example above, because class Square inherits from class Rectangle, we can use the following:

public class LSPExample1 {
    public void example2() {
        Rectangle rect = new Square();
        rect.setWidth(5);
        rect.setHeight(10);
        System.out.println(rect.calculateArea()); // 100
    }
}

The result is obviously wrong, the area of the rectangle should be 5 * 10 = 50.

According to this principle, we must ensure that when a class inherits from another class, it will not change the behavior of that class.

In this case, to avoid the LSP violation, we have to create a superclass, e.g. Shape class, and then for Square and Rectangle to inherit this Shape class.

Solution: Break the hierarchy

public interface Shape {
    int area();
}

class Rectangle implements Shape {
    private int width;
    private int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    @Override
    public int area() {
        return this.width * this.height;
    }
}

class Square implements Shape {

    private int side;

    public void setSide(int side) {
        this.side = side;
    }

    @Override
    public int area() {
        return this.side * this.side;
    }
}

Example 2 - a subclass changes the behavior of the parent class

class Product {
    protected double discount = 10;

    public double getDiscount() {
        return discount;
    }
}

class SecondHandProduct extends Product {
    public void applyExtraDiscount() {
        discount = discount * 1.5;
    }
}

class PricingCalculator {

    void showDiscount() {
        List<Product> products = Arrays.asList(
                new Product(),
                new SecondHandProduct()
        );
        products.forEach(product -> {
            if (product instanceof SecondHandProduct) { // Violate LSP
                SecondHandProduct secondHandProduct = (SecondHandProduct) product;
                secondHandProduct.applyExtraDiscount();
            }
            System.out.println(product.getDiscount());
        });
    }
}

As you can see, with this implementation we have to check the instance type and call another method before calling getDiscount(). It violates LSP.

Solution: "Tell, don't ask"

class Product {
    protected double discount = 10;

    public double getDiscount() {
        return discount;
    }
}

class SecondHandProduct extends Product {

    public double getDiscount() {
        applyExtraDiscount();
        return discount;
    }
    
    public void applyExtraDiscount() {
        discount = discount * 1.5;
    }
}

class PricingCalculator {

    void showDiscount() {
        List<Product> products = Arrays.asList(
                new Product(),
                new SecondHandProduct()
        );
        products.forEach(product -> {
            System.out.println(product.getDiscount());
        });
    }
}

Example 3 - subclass throws an exception

Another case of LSP scope is a subclass that throws an exception.

interface FileService {
    void getFiles();
    void deleteFiles();
}
 
class ImageFileService implements FileService {
 
    @Override
    public void getFiles() {
        // Load image files
    }
 
    @Override
    public void deleteFiles() {
        // Delete image files
    }
}
 
class TempFileService implements FileService {
 
    @Override
    public void getFiles() {
        // Load temp files
    }
 
    @Override
    public void deleteFiles() {
        // Delete temp files
    }
}

The above implementation classes have no problem, everything runs fine. Now we add a new SystemFileService class. With the requirement not allowed to delete the file system, this class will generate an UnsupportedOperationException error. A method that is designed but not used is not a good design either.

class SystemFileService implements FileService {
 
    @Override
    public void getFiles() {
        // Load temp files
    }
 
    @Override
    public void deleteFiles() {
        throw new UnsupportedOperationException();
    }
}

When executing the deleteFiles() method, the SystemFileService class throws an error at runtime. It cannot replace its parent FileService class, so it is already LSP violation.

public class LSPExample2 {
    public void example2() {
        FileService fileService = new FileService(); 
        fileService.deleteFiles(); // Violate LSP
    }
}

Solution: Create a new interface

Example 4 - subclass with an empty implementation

interface CrudRepository<T, ID> {

    Iterable<T> findAll();

    T findOne(ID id);

    T save(T entity);

    void update(T entity);

    void delete(ID id);

}

class UserRepository implements CrudRepository<User, Long> {

    @Override
    public Iterable<User> findAll() {
        // find all users ...
        return list;
    }

    @Override
    public User findOne(Long id) {
        // find user by id ...
        return user;
    }

    @Override
    public User save(User entity) {
        // save user ...
        return savedUser;
    }

    @Override
    public void update(User entity) {
        // update user ...
    }

    @Override
    public void delete(Long id) {
        // Set flag to delete user
    }
}

The above implementation looks good. However, we now have a MasterDataRepository, with requirements that the master data is read-only, cannot be added, modified, or deleted. We have the implementation as follows:

class MasterDataRepository implements CrudRepository<MasterData, Long> {

    @Override
    public Iterable<MasterData> findAll() {
        // find master data ...
        return list;
    }

    @Override
    public MasterData findOne(Long id) {
        // find master data by id ...
        return masterData;
    }

    @Override
    public MasterData save(MasterData entity) {
        // Do nothing, don't allow to add master data
        return null;
    }

    @Override
    public void update(MasterData entity) {
        // Do nothing, don't allow to delete master data
    }

    @Override
    public void delete(Long id) {
        // Do nothing, don't allow to delete master data
    }
}

Because class MasterDataRepository inherits from class CrudRepository, we can use the following:

class LSPExample3 {

    @Test
    public void example3() {
        CrudRepository<MasterData, Long> crudRepository = new MasterDataRepository();
        MasterData masterData = crudRepository.findOne(1);
        masterData.setDesc("Change description");
        crudRepository.update(masterData); // Do nothing, violate LSP
    }
}

Solution: Create a new interface

Last updated