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() { ... }
}
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
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
- Design Principles | Hello Interview Low Level Design. www.hellointerview.com . Accessed Sep 13, 2026.
- IEnumerable<T> Interface (System.Collections.Generic) | Microsoft Learn. learn.microsoft.com . Accessed Sep 13, 2026.
- ICollection<T> Interface (System.Collections.Generic) | Microsoft Learn. learn.microsoft.com . Accessed Sep 13, 2026.
- IReadOnlyCollection<T> Interface (System.Collections.Generic) | Microsoft Learn. learn.microsoft.com . Accessed Sep 13, 2026.
- IList<T> Interface (System.Collections.Generic) | Microsoft Learn. learn.microsoft.com . Accessed Sep 13, 2026.
- IReadOnlyList<T> Interface (System.Collections.Generic) | Microsoft Learn. learn.microsoft.com . Accessed Sep 13, 2026.
Remember that interfaces are composable. Only promise what the implementing classes can always deliver.