The Liskov Substitution Principle (LSP)

The original definition reads rather technical:

  • Let \(\Phi,(x)\) be a property provable about objects \(x\) of type \(T\). Then \(\Phi,(y)\) should be true for objects \(y\) of type \(S\) where \(S\) is a subtype of \(T\).

What is meant by this in the context of object-oriented programming is:

  • A derived classes should be usable everywhere where its superclass is used.

Alt text

Simple Example

Again from github.

Bad Practice

class Animal:
    def __init__(self, name: str):
       self.name = name

def animal_leg_count(animals: list[Animal]):
    for animal in animals:
        if isinstance(animal, Lion):
            print(4)
        elif isinstance(animal, Mouse):
            print(4)
        elif isinstance(animal, Pigeon):
            print(2)

class Lion(Animal):
    pass

animal_leg_count([Lion("Simba")])

What if someone introduces a new Animal Zebra without extending animal_leg_count()? General Remark: Having to request the instance of a class points towards a violation of the LSP

Good Practise

from abc import ABC,abstractmethod
#
# Example taken from https://gist.github.com/dmmeteo/f630fa04c7a79d3c132b9e9e5d037bfd
#
# "Open for Extension, closed for Modification"
#
class Animal(ABC):
    def __init__(self, name: str):
       self.name = name

    @abstractmethod
    def leg_count(self): # 
        pass
...
class Lion(Animal):
    def leg_count(self):
        return 4

def animal_leg_count(animals: list):
    for animal in animals:
        print(animal.leg_count())    

animal_leg_count([Lion("Simba")]])

The user now gets a nice error message if the number of legs of a new Animal has not been specified.

Examples @ Linde Engineering

Good Practise

A SMILE_Model_no_deriv is usable anywhere where a SMILE_Model can be used.

Bad Practice @ LE (is it though?))

In SMILE, a specific unit operation is generally derived from a Base class. The Split model in SMILE is defined as derivation from SMILE_Model_Adept.

However, a Split is obviously not usable everywhere where a SMILE_Model_CppAD may be used.

I think this can be considered this a boundary case, as SMILE_Model_CppAD is an abstract base class and hence an interface rather than a class; in contrast Split is a instance which complies to it.

General Remarks

Bad Practise

Deriving from C++ standard containers is generally bad practise.

One sometimes see code such as

struct my_vector : public std::vector<double> {
    my_vector(size_t myLen) : std::vector<double>(myLen) {};
};

This is a violation of the LSP, as myvector cannot be everywhere where a std::vector can. For instance, it no longer has a default ctor, which is a quite fundamental property of standard containers.

Hint: C++11 allows to mark a class marked to forbids deriving from it (keyword final) a class and hence violating the LSP. Then, why is std::vector not marked final? The only reason is that that would break too much (bad?) code - see here

Further Reading

Alt text