OOP SOLID Principles "L" - Liskov Substitution Principle

According to the Wikipedia the Liskov Substitution Principle (LSP) is defined as:

Subtype Requirement:
Let f(x) be a property provable about objects x of type T.
Then f(y) should be true for objects y of type S where S is a subtype of T.

The basic idea - if you have an object of type T then you can also use objects of its subclasses instead of it.

Or, in other words: the subclass should behave the same way as the base class. It can add some new features on top of the base class (that's the purpose of inheritance, right?), but it can not break expectations about the base class behavior.

The expectations about the base class can include:

  • input parameters for class methods
  • returned values of the class methods
  • exceptions are thrown by the class methods
  • how method calls change the object state
  • other expectations about what the object does and how

Some of these expectations can be enforced by the programming language, but some of them can only be expressed as the documentation.

This way to follow the LSP it is not only important to follow the coding rules, but also to use common sense and do not use the inheritance to turn the class into something completely different.

Let's see what rules do we need to follow in the code.

Methods Signature Requirements

Signature requirements are requirements for input argument types and return type of the class methods.

Let's imagine we have following class hierarchy:

   .------------.          .------------.          .-------------.
   | LiveBeing  |          |   Animal   |<|--------|     Cat     |
   |------------|<|--------|------------|          |-------------|
   | + breeze() |          | + eat()    |<|---.    | + mew()     |
   '------------'          '------------'     |    '-------------'
                                              |
                                              |     .------------.
                                              |     |    Dog     |
                                              '-----|------------|
                                                    | + bark()   |
                                                    '------------'

Here the LiveBeing is the base class which is inherited by Animal which in turn is inherited by Cat and Dog.

I will use this hierarchy to explain the signature rules.

Covariance (Parent -> Child -> ...) of return types in the subtype

This rule means that the child class can override a method to return a more specific type (Cat instead of Animal).

Of course, it can return the same type, but it can not return more generic type (like LiveBeing instead of Animal) and it can not return a completely different type (House instead of Animal).

This rule is easy to understand and it feels natural. Here is an example in pseudo-code:

class Owner
  Animal findPet()
      return new Animal()

class CatOwner extends Owner
  Cat findPet()
      # Covariance - subclass returns more specific type
      return new Cat()

class BadOwner extends Owner
  LiveBeing findPet()
      # Contravariance - breaks the rule and returns more generic type
      return new LiveBeing()

function doAction(Owner owner)
    # OK for Owner, we can put an Animal object into the `animal` variable.
    # OK for CatOwner, we can put a Cat object into the `animal` variable.
    # Problem for a BadOwner object, a LiveBeing object can not use be used
    # the same way as Animal object.
    Animal animal = owner->findPet();
    amimal->eat();

The doAction function demonstrates a possible use case. It is OK if owner is a CatOwner, because both Animal and Cat should behave the same.

But the BadOwner returns a LiveBeing and it is a problem. There is no guarantee that LiveBeing object behaves the same as Animal.

For example, if we call animal->eat() this will not work for the LiveBeing (it doesn't have such a method).

Contravariance (Child -> Parent -> ...) of method arguments in the subtype

This means that a child class can override the method to accept a more generic argument type than the method in the base class (like accept the LiveBeing instead of Animal).

class Owner
  void feed(Animal animal)
    ...

class GoodOwner extends Owner
  # Contravariance - subclass accepts more generic type
  void feed(LiveBeing being)
    ...

class BadCatOwner extends Owner
  void feed(Cat cat)
    ...

function doAction(Owner owner)
  owner->feed(new Dog) # OK for Owner, he accepts any Animal, including the Dog
                       # OK for GoodOwner, he accepts any LiveBeing, including the Dog
                       # Problem for CatOwner, he doesn't expect the Dog

In practice, it may feel tempting to break this rule and define the class like BadCatOwner above.

But, as we can see, the BadCatOwner breaks LSP and we can not use it in the same case where we can use the Owner object.

Note that although using the more generic type in the subclass is OK in terms of method signature, it may be problematic logically:

class Owner
  void feed(Animal animal)
    animal->eat(this->findFood());

class GoodOwner extends Owner
  void feed(LiveBeing being)
    # Problem: LiveBeing doesn't have the `eat` method
    being->eat(this->findFood());

There is a problem here - the GoodOnwer::feed can not call the being->eat() method because LiveBeing doesn't have the eat method.

And this way, GoodOwner also can not just forward the execution to the parent method with something like parent::feed(being).

By the way, if the method doesn't use parent implementation, it may indicate the LSP violation - potentially we can have a different behavior for this subtype than in the parent class.

Exceptions should be same or subtypes of the base method exceptions

No new exceptions should be thrown by methods of the subtype, except where those exceptions are themselves subtypes of exceptions thrown by the methods of the parent type.

class BadFoodException

class BadCatFoodException extends BadFoodException

class LowQualityFoodException


class Owner
  void feed(Animal animal, Food food)
    if (not this->isGoodFood(food))
      throw BadFoodException()
    ...

class BadOwner extends Owner
  void feed(Animal animal, Food food)
    if (not this->isHighQualityFood(food))
      throw LowQualityFoodException()
    ...

function doAction(Owner owner)
  try
    owner->feed(new Dog, new SomeFood)
  catch (BadFoodException error)
    # OK for Owner, it can raise BadFoodException
    # OK for CatOwner, it can raise BadCatFoodException (subclass of BadFoodException)
    # Problem for BadOwner, it can raise LowQualityFoodException and it will not be
    # caught here
    ...

If we don't follow the rule about exception types, the client code written for the base class Owner will fail for the subclass BadOwner and this way we violate the LSP.

Inheritance requirements

These requirements describe additional rules for inherited methods related to the Design by contract concept. It defines the "contract" for each method which includes preconditions, postconditions and invariants:

  • Precondition is a condition or predicate that must always be true just prior to the execution of some section of code.
  • Postcondition is a condition or predicate that must always be true just after the execution of some section of code
  • Invariant is a condition that can be relied upon to be true during execution of a program, or during some portion of it. For example, a loop invariant is a condition that is true at the beginning and end of every execution of a loop.

Preconditions cannot be strengthened in a subtype

In most cases preconditions are expectations about method input arguments, also an object's internal state can be a part of the precondition.

This is a more generic rule of the contravariance rule for method arguments. The contravariance rule says that subclass can accept more generic argument type (LiveBeing instead of Animal), this is a weaker precondition (subclass accepts a wider range of arguments).

The same logic applies not only to the types of arguments but to the other kind of expectations as well, such as a range of the integer argument:

class The24Hours
  void setHour(int hour)
    # hour should be between 0 and 23
    assert (0 <= hour and hour <=23)
    ...

class TheDay extends The24Hours
  void setHour (int hour)
    # breaks the rule and strengthens the precondition
    # day hour should be between 8 and 16
    assert (8 <= hour and hour <= 16)
   ...

function doAction(The24Hours hours)
  hours->setHour(3); # OK for `The24Hours` object
                     # Problem for `TheDay` - it will raise an error

So the stronger precondition in the child class broke the client code which worked for the parent class.

At the same time, if we make the precondition weaker (or even remove it), the client code will work the same way as for parent class.

Postconditions cannot be weakened in a subtype

Postconditions are usually expectations related to the method return value.

Again, this the more generic rule similar to the covariance rule (method can return Cat instead of Animal), the postcondition is strengthened.

An example of postcondition rule violation:

class The24Hours
  number setHour(number hour)
    ...
    assert (this.hour is integer)
    return this.hour

class TheTime extends The24Hours
  number setHour(number hour, number hourFraction)
    this.hour = hour + hourFraction / 100
    # the postcondition is weaker (float is a wider area than integer)
    assert (this.hour is float)
    return this.hour

function doAction(The24Hours hours)
  int result = hours->setHour(3); # OK for The24Hours
                                  # Problem for TheTime, it returns float

So again, due to LSP violation, we can not use the child class instead of the parent.

Invariants of the parent type must be preserved in a subtype

Invariant is something that is not changed during the method execution. It can be the whole or part of the object internal state:

class The24Hours
  # invariant: this.hour is not changed
  number getHour()
    return this.hour

class TheCounter extends The24Hours
  # invariant violation: this.hour is changed
  number getHour()
    result = this.hour
    this.hour += 1
    return result

function doAction(The24Hours hours)
  if (hours->getHour() <= 12)
     # OK for The24Hours
     # Problem for TheTime, now getHour() can return value > 12
     print 'First half of the day', hours->getHour()

History constraint (the "history rule")

The subtypes should not introduce new methods that will allow modifying the object state in a way that is not possible for the parent class.

The internal object state should be modifiable only through their methods (encapsulation) and the client code can have some expectations as of the possible ways to modify the internal state.

For example:

class Time
  # it is immutable, once time is set, there is no way to change it
  constructor(int hour, int minute)
  getTime()

class FlexibleTime extends Time
  # violates the "history" rule
  # it allows changing the object state
  # but the clients who use Time can be broken due to this
  setTime(int hour, int minute)

doAction(Time time)
  print 'Now it is: ', time->getTime()
  doOtherAction(time)
  # OK for Time, it can not be changed, so value is the same
  # Problem for FlexibleTime, the `doOtherAction` could change it
  print 'Now it is still: ', time->getTime()

Links

Wikipedia:Liskov substitution principle

Agile Design Principles: The Liskov Substitution Principle

Wikipedia: Covariance and contravariance