Liskov Substitution Principle

Dated Sep 13, 2026; last modified on Sun, 13 Sep 2026

Liskov Substitution Principle

Subclasses must work wherever the base class works. The subclass can add new behavior, but it can’t remove or break behavior that the parent class promised.

Instead of:

public class Bird
{
  public virtual void Fly() { ... }
}

public class Penguin : Bird
{
  public override void Fly() => throw new NotSupportedException("Penguins can't fly");
}

… separate the flying behavior so that it’s only implemented by birds that can fly:

public interface IBird
{
  void Eat();
}

public interface IFlyingBird : IBird
{
  void Fly();
}

public class Sparrow : IFlyingBird
{
  void Eat() { ... }
  void Fly() { ... }
}

public class Penguin : IBird
{
  void Eat() { ... }
}

Remember that interfaces are composable. Only promise what the implementing classes can always deliver.

Interface Segregation Principle

The rationale for the Interface Segregation Principle rhymes a lot with that of the Liskov Substitution Principle, and thus melding the two here.

Prefer small, focused interfaces over large, general-purpose ones. If a class only needs two methods from an interface with ten methods, that interface is too big.

For example, do not have:

public interface IWorker
{
  void Work();
  void Eat();
  void Sleep();
}

… as that leads to:

public class RobotWorker : IWorker
{
  public void Work() { ... }
  public void Eat() => throw new NotSupportedException("Robots don't eat");
  public void Sleep() => throw new NotSupportedException("Robots don't sleep");
}

Instead, have thin interfaces, e.g.,

public interface IWorkable
{
  void Work();
}

public interface IFeedable
{
  void Eat();
}

public interface IRestable
{
  void Sleep();
}

public class Human : IWorkable, IFeedable, IRestable { ... }

public class Robot : IWorkable { ... }

Case Study: .NET’s Collection Interfaces

Materials like tend to give toy examples to illustrate the concept. .NET gives us a real world example.

Collections implementing IEnumerable<T> can be enumerated using the foreach statement. The only method in this interface is GetEnumerator().

ICollection<T> implements IEnumerable<T> but adds properties like Count and IsReadOnly, and methods like Add(T), Remove(T), Clear(), Contains(T), and CopyTo(T[], Int32). On the same hierarchy, IReadOnlyCollection<T> also implements IEnumerable<T>, but only adds the Count property.

It’s a bit awkward to have an API that exposes an ICollection<T> whose IsReadOnly == true. The Liskov Substitution Principle expects that code receiving ICollection<T> should be able to call Add(T) without getting an error.

IList<T> implements ICollection<T> and represents a collection of objects that can be individually accessed by index. It adds the property Item[Int32], and methods like IndexOf(T), Insert(Int32, T), and RemoveAt(Int32). IReadOnlyList<T> implements IReadOnlyCollection<T> and represents a read-only collection of elements that can be accessed by index; it only adds the Item[Int32] getter property.

References

  1. Design Principles | Hello Interview Low Level Design. www.hellointerview.com . Accessed Sep 13, 2026.
  2. IEnumerable<T> Interface (System.Collections.Generic) | Microsoft Learn. learn.microsoft.com . Accessed Sep 13, 2026.
  3. ICollection<T> Interface (System.Collections.Generic) | Microsoft Learn. learn.microsoft.com . Accessed Sep 13, 2026.
  4. IReadOnlyCollection<T> Interface (System.Collections.Generic) | Microsoft Learn. learn.microsoft.com . Accessed Sep 13, 2026.
  5. IList<T> Interface (System.Collections.Generic) | Microsoft Learn. learn.microsoft.com . Accessed Sep 13, 2026.
  6. IReadOnlyList<T> Interface (System.Collections.Generic) | Microsoft Learn. learn.microsoft.com . Accessed Sep 13, 2026.