Understanding SOLID Principles: Liskov Substitution

This post is continuation of the SOLID principles series that I have been writing about on SOLID Principles.

Please make sure you read my other blogs on the topic

Liskov substitution principle (LSP) basically deals with inheritance hierarchies in which a client can reliably use any class or subclass without compromising the expected behavior.

Formal Definition

The definition of the LSP by prominent computer scientist Barbara Liskov is a bit dry, so it requires further explanation. Here is the official definition

If S is a subtype of T, then objects of type T may be replaced with object of type S, without breaking the program.

Barbara Liskov

Above definition may sound little bit cryptic, Lets try to understand further

Base Type: The type (T ) that clients have reference to. Clients call various method, any of which can be overridden by the subtype.

Sub Type: Any one of possible family of classes ( S ) that inherit from the base type (T ). Clients should not know which specific subtype they are calling, nor should they need to. The client should behave the same regardless of the subtype instance that is given.

Still unclear? Let’s see an example

Consider the below code

class Bird { }
class Dove { 
    void Fly() { }
}

The Dove can fly because its a bird, but what about this

class Penguin : Bird {}

Penguin is bird but it can’t fly, Penguin and Dove both are subclass of Bird, but we can’t substitute Dove with Penguin.

We can fix this in multiple ways either by introducing a FlyingBirds SubType or by introducing IFlyable interface etc

public class Bird{}
public class FlyingBirds : Bird{
    public void fly(){}
}
public class Dove : FlyingBirds{}
public class Penguin : Bird{} 

There are several “rules” that must be followed for LSP compliance. These rules can be split in to two categories: contract rules (expectation) and variance (types that can be substituted)

Contract rules

  • Preconditions cannot be strengthened in a subtype
  • Post conditions cannot be weakened

Preconditions are defined as all of the conditions necessary for a method to run reliably and without fault. Every method requires some preconditions to be true before it should be called.

Example of a precondition

public decimal CalcualteShippingCost(
    float packageWeightInKilograms,
    Size<float> packageDimensionsInInches,
    RegionInfo destination)
{
   if(packageWeightInKilograms <= 0f) throw new ArgumentOutOfRangeException("packageWeightInKilograms", "Package weight must be positive and non-zero");
  //....
}

Postconditions check whether an object is being left in a valid state as a method is exited. Whenever state is mutated in a method, it is possible for the state to be invalid due to logic errors

Variance rules

  • There must be contravariance of the method arguments in the subtype
  • There must be covariance of the return types in the method

Conclusion

On the surface, Liskov substitution principle may sound complex, but if we understood the concepts of contracts and variance to build rules your could make your code more adaptive.

References

Adaptive Code via C#

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s