SOLID in Action — the Liskov Substitution Principle

Subclasses should be substitutable for their base classes

Gerald Nguyen
Gerald Nguyen
7 min read ·
Previous | Next
Also on Medium
On this page

Let us start with a question:

Can social media connections substitute for real life friends and family?

Well, it depends on how we define “connection”. If we have it as below, then according to the Liskow Substitution Principle (LSP), the answer is No.

Connection classes

Connection classes

Before we go further into this example, let’s back it up a bit to study the official definition of LSP.

There is some math but I promise to keep it brief

The Liskov Substitution Principle (LSP)

The LSP was first introduced by Barbara Liskov in 1988 and was further refined in 1994 together with Jeannette Wing. The LSP defines a strong behavioral subtyping relation between parent and child classes.

It goes beyond the syntactic substitution of the parent-child relationship to emphasize the semantic compliance of child classes to their parent.

The 1994 definition is below:

https://dl.acm.org/doi/pdf/10.1145/197320.197383

https://dl.acm.org/doi/pdf/10.1145/197320.197383

Meaning, if S is a subtype of T, then whatever property (characteristic in both syntactic and semantic manners) that is true for instances of T should also be true for instances of S.

Syntactic substitution

Syntactic substitution is a necessary condition for LSP. The following conditions should hold true when validating and enforcing method/function signatures:

Let’s examine how the method findMutual(Person): List<Connection> in the parentConnection is overridden by findMutual(Subject): List<SocialConnection>in the child SocialConnection. We will pay special attention to how the co-variance of the return type and contra-variance of parameter types differ in the child from the parent.

Let’s look at the below sample code:

class News {}   
class Subject {}  
class Person extends Subject {}  
  
class Connection {  
    constructor(public name: string, public since: number) {}  
      
    consumeNews(news: News) {}  
  
    findMutual(entity: Person): Connection\[\] {  
        console.log("Print from Connection", entity);  
        return \[\];  // mock values for now  
    }  
}  
  
class SocialConnection extends Connection {  
    constructor(public platform: string, name: string) {  
        super(name, Date.now())  
    }  
  
    override findMutual(entity: Subject): SocialConnection\[\] {  
        console.log("Print from SocialConnection", entity);  
        return \[\]; // mock values for now  
    }  
}

Notice the following:

Co-variance and contra-variance are complex concepts. The above illustration only touches briefly on the surface. Please visit https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science) for a more detailed explanation.

To my knowledge, support for syntactic substitution in programming languages such as Java, Kotlin, and C# is not complete, at least for the contra-variance of the method parameter types. The tricky challenge there is the way these programming languages handle multiple methods with the same name. If the parameter types in the child’s method differ, the child’s method overloads rather than overrides the parent’s method. That leads to multiple active implementations of the same method in the child type instead of one, as the LSP specifies. More relaxed languages such as Javascript and Typescript (which I used in the above example) do not have that problem, though they lack the strong type-safety provided by the formers.

Preconditions, Postconditions, Invariants

The LSP closely resembles the Design-by-Contract approach first introduced by Bertrand Meyer in its requirements for the Preconditions, Postconditions, and Invariants:

When applying to syntactic substitution, these requirements map to contra-variance, co-variance, and exception requirements respectively:

Of course, there is more to them than just syntax compliance or type safety:

The Invariant condition can have applications beyond the boundary of operations. A common example is class invariant conditions. From our very first example, if 2-way interaction is an invariant of Connection, then the Follower type has failed that invariant condition while SocialConnection, RealLifeConnection, Friend, and Family all satisfy it.

The History Constraint

Compared to Design-by-Contract, the LSP has an additional History constraint.

The History constraint prohibits unauthorized modification of inherited state in the subtype. If the child class contains any new method that directly modifies the inherited state, such modification is considered unauthorized and thus violates the History constraint.

This constraint, in my opinion, is the merger of Encapsulation and Invariant. Good object-oriented design promotes Encapsulation to maintain a consistent and usable object’s state through provided, thus authorized, mechanism. In a subtype, that consistency and usability through encapsulation should be preserved — the Invariant.

Recall from our code example above SocialConnection inherits the since and thename properties from Connection. Because since denotes a fact, it must be unchanged after construction. However, because of the way these classes are written, it is possible for SocialConnection to introduce a method that modifies the inherited since property. That would violate the History constraint.

Alarm bell BUGGGG!
Calm down. Just re-declare since with the readonly modifier i.e. public readonly since: number

Side joke: It’s not a bug, it is a feature! Source: internet — nobody knows the original source

Side joke: It’s not a bug, it is a feature! Source: internet — nobody knows the original source

Conclusion

The LSP touches on multiple aspects of object-oriented design. The requirements altogether cross the syntactic compliance to the semantic realm.

Following the LSP ultimately leads us to strong behavioral substitutability which is a desired property in software development. Behavioral substitutability enables not just type-safetiness but the assertion about expected behavior. These outcomes enable not only the confidence that our program behaves correctly but also the ability to prove its correctness through Hoare logic (which is a topic for another article).

Generated by AI DALL.E

Generated by AI DALL.E

If you like this article, please follow me for more quality content.

Other articles in this series:

Thank you.