Liskov Substitution Principle

The Liskov Substitution Principle (LSP) is one of the SOLID principles of object-oriented design. It states that "objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program"

Understanding the Liskov Substitution Principle (LSP)

  • LSP states that the child class should be perfectly substitutable for their parent class. If class B is derived from class A then B should be substitutable for A.
  • If a program or module is using a base class, then the derived class should be able to extend its base class without changing its original implementation.

In simpler terms, this means that a subclass should be able to substitute its superclass without causing errors or unexpected behavior.

Examples of Liskov Substitution Principle in C#

Bad Example (Violation of LSP)

// Base class
public class Bird
{
    public virtual void Fly()
    {
        Console.WriteLine("A bird is flying");
    }
}

// Derived class violating LSP
public class Ostrich : Bird
{
    public override void Fly()
    {
        throw new InvalidOperationException("Ostriches cannot fly");
    }
}

Explanation:

In this example, the Ostrich class violates Liskov Substitution Principle because it overrides the Fly() method from the base class Bird to throw an exception indicating that ostriches cannot fly. This contradicts the expected behavior defined in the base class (Bird). If client code expects all Bird objects to be able to fly and substitutes an Ostrich object, it will encounter unexpected behavior (an exception) instead of the intended behavior (flying). This violates the principle that subclasses should extend rather than contradict the behavior of the base class.

Good Example

// Base class
public abstract class Vehicle
{
    public abstract void Start();
    public abstract void Accelerate();
    public abstract void Brake();
}

// Derived classes
public class Car : Vehicle
{
    public override void Start()
    {
        Console.WriteLine("Car started");
    }

    public override void Accelerate()
    {
        Console.WriteLine("Car accelerating");
    }

    public override void Brake()
    {
        Console.WriteLine("Car braking");
    }
}

public class Bicycle : Vehicle
{
    public override void Start()
    {
        Console.WriteLine("Bicycle started pedaling");
    }

    public override void Accelerate()
    {
        Console.WriteLine("Bicycle accelerating");
    }

    public override void Brake()
    {
        Console.WriteLine("Bicycle braking");
    }
}

Explanation:

The Vehicle base class defines common behaviours (Start(), Accelerate(), Brake()) that any vehicle should have. Subclasses (Car and Bicycle) inherit from Vehicle and provide their specific implementations. This adheres to Liskov Substitution Principle because instances of Car and Bicycle can be substituted for Vehicle without affecting the expected behavior of starting, accelerating, or braking.


Analysis

The key to understanding Liskov Substitution Principle is ensuring that subclasses honor the contracts established by their base classes. In the good example (Vehicles), both Car and Bicycle extend the behavior of Vehicle by providing their own implementations of common vehicle actions. This allows them to be used interchangeably wherever a Vehicle is expected.

In contrast, the bad example (Birds) with Ostrich shows how overriding a method (Fly()) in a way that contradicts its expected behavior in the base class (Bird) can lead to violations of Liskov Substitution Principle. This can cause unexpected behavior or errors when substituting subclasses for their base class instances.


Conclusion

By adhering to Liskov Substitution Principle in your designs, you ensure that your object-oriented code remains flexible, maintainable, and robust. It encourages a hierarchy where subclasses extend rather than contradict the behaviour of their base classes, fostering cleaner and more predictable software systems.