SOLID Design Principles in C# - Part 3 - Liskov Substitution

2022, Sep 13

The Liskov Substitution Principle (LSP) is a principle that helps us to think about extending class behaviours, and this is applied in object-oriented programming (OOP) through inheritance, interfaces or composition.

This post is part of a series where we explore the SOLID design principles, arguably the most popular design principles for object-oriented software development.

Liskov Substitution Principle

Derived classes must be substitutable for their base classes. Robert C. Martin

Liskov Substitution Principle, written by Barbara Liskov in 1988, states the following: "if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program". In other words, any object of some class in an object-oriented program can be replaced by an object of a child class.

When applying this principle, inheritance and polymorphism should be carefully contextualized, otherwise, we can easily violate this principle.

The wrong example

When abstractions are not well thought out, when systems increase in complexity and refactoring is not applied you may notice violations like that:

public class Bird
{
    public string Name { get; }

    public Bird(string name)
    {
        Name = name;
    }

    public virtual void Eat()
    {
        Console.WriteLine($"{Name} eats.");
    }

    public virtual void LayEggs()
    {
        Console.WriteLine($"{Name} lays eggs.");
    }

    public virtual void Fly() // can all birds fly?
    {
        Console.WriteLine($"{Name} can fly.");
    }
}

Case 1: Because the abstraction wasn't well defined, exceptions are raised to justify an impossible action:

public class Duck : Bird
{
    public Duck(string name) : base(name)
    {
    }
}

public class Kiwi : Bird
{
    public Kiwi(string name) : base(name)
    {
    }

    public override void Fly()
    {
        throw new Exception("Kiwi birds can't fly.");
    }
}

Case 2: Because it is legacy, some devs skipped the implementation by doing nothing:

public class Penguin : Bird
{
    public Penguin(string name) : base(name)
    {
    }

    public override void Fly()
    {
        // Do nothing
    }
}

Looking at the implementations above, if we were to execute them in a Program.cs, unlike any other Principle violation that would result in bad but working code, this would lead to a buggy, difficult-to-maintain code.

The right example

There are some ways to fix the previous implementations:

  1. Leave the base class with only the minimal base behaviours, and leave the specialization in the derived class(es).
public class Bird
{
    public string Name { get; }

    public Bird(string name)
    {
        Name = name;
    }

    public virtual void Eat()
    {
        Console.WriteLine($"{Name} eats.");
    }

    public virtual void LayEggs()
    {
        Console.WriteLine($"{Name} lays eggs.");
    }
}

public class Duck : Bird
{
    public Duck(string name) : base(name)
    {
    }

    public void Fly()
    {
        Console.WriteLine($"{Name} can fly.");
    }
}

public class Kiwi : Bird
{
    public Kiwi(string name) : base(name)
    {
    }
}
  1. Use granular interfaces to dictate a contract of the implementation. By the way, this is related to the next principle Interface Segregation Principle (ISP).
public interface IRegularBird
{
    string Name { get; }
    void Eat();
    void LayEggs();
}

public interface IFlyingBird
{
    string Name { get; }
    void Fly();
}

public class Bird : IRegularBird
{
    public string Name { get; }

    public Bird(string name)
    {
        Name = name;
    }

    public virtual void Eat()
    {
        Console.WriteLine($"{Name} eats.");
    }

    public virtual void LayEggs()
    {
        Console.WriteLine($"{Name} lays eggs.");
    }
}

public class FlyingBird : IRegularBird, IFlyingBird
{
    public string Name { get; }

    public FlyingBird(string name)
    {
        Name = name;
    }

    public virtual void Eat()
    {
        Console.WriteLine($"{Name} eats.");
    }

    public virtual void LayEggs()
    {
        Console.WriteLine($"{Name} lays eggs.");
    }

    public virtual void Fly()
    {
        Console.WriteLine($"{Name} can fly.");
    }
}

public class Kiwi : Bird
{
    public Kiwi(string name) : base(name)
    {
    }
}

public class Duck : FlyingBird
{
    public Duck(string name) : base(name)
    {
    }
}

I invite you to have a look at the repository and run the examples:

The Liskov Substitution principle example is available on PlayGoKids repository.

Full Series

Single-Responsibility Principle (SRP)

Open-Closed Principle (OCP)

Liskov Substitution Principle (LSP)

Interface Segregation Principle (ISP)

Dependency Inversion Principle (DIP)