// SOLID & Principles · 03
Liskov Substitution
A subtype must keep every promise its base type makes.
How it fits together
- ReportQuery uses / calls Repository<T> (Add(item))
- ReadOnlyRepository implements Repository<T>
- ReadOnlyRepository fails / error path ReportQuery (throws)
- Repository<T> implements ReadRepository<T>
- Repository<T> implements WriteRepository<T>
- ReportQuery uses / calls ReadRepository<T> (Get / List)
Spot it
- Overrides that throw "not supported" errors
- Callers check the concrete type before calling
- A derived class quietly ignores a method
It costs
Honest hierarchies are often flatter; sometimes the fix is dropping inheritance entirely.
In code
public interface IRepository<T>{ Task AddAsync(Guid id, T item, CancellationToken ct); Task<T?> GetAsync(Guid id, CancellationToken ct);}// One contract every implementation must pass. A type that can't is not a subtype.public abstract class RepositoryContract{ protected abstract IRepository<Invoice> Create(); [Fact] public async Task GetAsync_AfterAdd_ReturnsTheItem() { // Arrange var repository = Create(); var invoice = new InvoiceBuilder().Build(); // Act await repository.AddAsync(invoice.Id, invoice, CancellationToken.None); var found = await repository.GetAsync(invoice.Id, CancellationToken.None); // Assert found.ShouldBe(invoice); }}public sealed class SqlInvoiceRepositoryContract : RepositoryContract{ protected override IRepository<Invoice> Create() => new SqlInvoiceRepository(TestDb.Connect());}public sealed class InMemoryInvoiceRepositoryContract : RepositoryContract{ protected override IRepository<Invoice> Create() => new InMemoryInvoiceRepository();}Go deeperThe full explanation and what to ask for · 1 min readExpandCollapse
Barbara Liskov's rule is about behaviour, not signatures: anywhere the base type works, the subtype must work too, without the caller knowing which one it has.
The classic trap is Square inheriting Rectangle. It compiles, but setting the width of a square also changes its height, breaking what callers of Rectangle rely on. "Is-a" in the real world is not "is substitutable" in code.
A subtype may accept more and promise more, never require more or deliver less. When a type cannot honour the contract, it should not share it.
Ask for it
- behavioural subtyping
- preconditions
- postconditions
- invariants
- contract tests
Check every implementation of the repository interface for Liskov violations: overrides that throw, strengthen preconditions, weaken postconditions or break invariants. Write one shared contract test suite all implementations must pass, and propose a split interface where one cannot.