SOLID and Liskov Substitution Principle

Overview

In this series of articles, I will cover all the SOLID patterns and their use in examples.

The SOLID principles were introduced by Robert C. Martin in his 2000 paper “Design Principles and Design Patterns.”

The SOLID principles consist of the following five concepts:

Single Responsibility Principle
Open/Closed Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle

lsp

Liskov Substitution Principle

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

Why

Firstly as you can see in previous principles srp and ocp inheritance can help us in general to solve many problems. It helps us to keep our code base small and I can focused on interfaces or classes. I can make classes and interfaces independent and less coupled.

However, I have to be careful when I am making connections between components. I can easily slip into an incorrect relationship between types and objects. Therefore, this principle is always about the relationship between all components.

Code Smells

For better understanding I can take a look on code smells which often indicates some violation of this principle. And can help to understand this principle.

A Subtype Throws an Exception for a Behavior It Can’t Fulfill

It happens often when developer implements interface or extend class and do not fill all override methods with new code. Instead of code it will throw an exception. I would not be useful for client code, where is excepted value.

public abstract class Account {
    protected abstract void deposit(BigDecimal amount);
    protected abstract void withdraw(BigDecimal amount);
}

public class FixedTermDepositAccount extends Account {
    @Override
    protected void deposit(BigDecimal amount) {
        // Deposit into this account
    }

    @Override
    protected void withdraw(BigDecimal amount) {
        throw new UnsupportedOperationException("Withdrawals are not supported by FixedTermDepositAccount!!");
    }
}
Empty method or functions

In some scenarios developer would not implement nothing at all. A Subtype Provides No Implementation for a Behavior It Ca not Fulfill.

public interface FileSystem {
    File[] listFiles(String path);
    void deleteFile(String path) throws IOException;
}

public class ReadOnlyFileSystem implements FileSystem {
    public File[] listFiles(String path) {
        // code to list files
        return new File[0];
    }

    // deleteFile operation is not supported on a read-only file system
    public void deleteFile(String path) throws IOException {
        // Do nothing.
    }
}
Type checking

Is some special occasions, clients knows about different types of object in hierarchy and check in their code for specific type of object (java word instanceof). This violation of liskov principle too.

for (Task t : tasks) {
    if (t instanceof BugFix) {
        Bugfix bf = (BugFix) t;
        bf.initBugDescription();
    }
    t.setInProgress();
}
Return the same value

And worst case is when override method return the same value. This behavior is hard to find and can easy slide into huge bug. In generally hardcoding values should have very good reason be in that place and in changeable state should not be presented at all.

public abstract class Car {
    // use fuel for running car
    private int fuel;
}

public class ToyCar extends Car {

    @Override
    protected int getRemainingFuel() {
        return 0;
    }
}

Implementation Strategies or how to avoid code smells

Now we can move on implementation strategies.

In case of existing code there are three options:

Delete relation between classes

Most common and most recommend to apply is remove existing inheritance relation between components.

Interface segregation principle

There will be separated article about that. In short terms: create connection with new interface or class.

Tell, don’t ask

In short summary, this principle tells you that instead of asking object for data you should tell it what should do and then wait for the result of the operation.

In other case when you need to implement new functionality or new code, you can follow these rules:

class BugFix extends Task {

    @Override
    public void setInProgress() {
        this.initBugDescription();
        super.setInProgress();
    }
}

# From previous example, regarding `BugFix` and `Task`

for (Task t : tasks) {
    t.setInProgress();
}

Signature Rule

Method arguments types

This rule states that the overridden subtype method argument types can be identical or wider than the supertype method argument types.

public abstract class Foo {
    public abstract Number generateNumber();
}

public class Bar extends Foo {
    @Override
    public Integer generateNumber() {
        return new Integer(10);
    }
}

Return types

(Covariance) The return type of the overridden subtype method can be narrower than the return type of the supertype method.

Exceptions

The subtype method can throw fewer or narrower exceptions than the supertype method.

Properties rule

Class invariants

A class invariant is an assertion concerning object properties that must be true for all valid states of the object.

History constraint

The history constraint states that the subclass methods (new or inherited) should not allow state changes that the base class did not allow.

public abstract class Car {
    // Allowed to be set once at the time of creation.
    // Value can only increment thereafter.
    // Value cannot be reset.
    protected int mileage;

    public Car(int mileage) {
        this.mileage = mileage;
    }
}
public class ToyCar extends Car {
    public void reset() {
        mileage = 0;
    }
}

This can be fixed by made mileage immutable.

Michal Slovík
Michal Slovík
Java developer and Cloud DevOps

My job interests include devops, java development and docker / kubernetes technologies.