Skip to content
Gustavo's Blog
Go back

SOLID — Liskov Substitution & Design by Contract

When developing solutions based on the Open/Closed Principle, we often turn to inheritance. We explored various strategies to apply OCP, but there’s a fundamental layer to consider: the design rules of inheritance. How do you know if your inheritance is correctly implemented?

That’s where the Liskov Substitution Principle comes in.

This principle states that subtypes must be substitutable for their base types. In other words, if you have a function f(x) where x is of the base type, then substituting x with any derived class should not change the behavior of f. As Barbara Liskov stated in 1988:

“If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T.”

             Program P
            /         \
    A                       B
  Classes             Expected Behavior

  o2 <base-class>  ──f(x)──▶  b
  o1 <derivative>  ──f(x)──▶  c

The substitution must hold. If it doesn’t, you have a design problem.

The Bank Account Example

Suppose we have three types of accounts:

The base account exposes two virtual methods:

public virtual void Deposit(decimal amount) { }
public virtual void Withdraw(decimal amount) { }

SavingsAccount implements these methods according to their signatures — no problem. But FixedDepositAccount presents an issue: it should not allow withdrawals. So its implementation ends up being:

public override void Withdraw(decimal amount) {
    throw new InvalidOperationException();
}

Now consider this service layer method:

public void ProcessWithdrawal(Account account, decimal amount) {
    account.Withdraw(amount);
}

We have a serious problem. The expected behavior of the withdrawal process, as defined by stakeholders, is not followed for a fixed deposit account because it throws an exception. The FixedDepositAccount subtype is not suitable as a replacement for the base Account class.

To fix this, you might consider an if statement to handle different account types — but that would also violate OCP. You’re stuck.

This example underscores the importance of domain knowledge. At first glance, it might seem obvious to use inheritance based on the “IS-A” relationship — a fixed deposit account is an account. But it’s crucial to examine whether the behaviors of these classes align. If they do not, the inheritance is inappropriate. As Uncle Bob states:

“The IS-A relationship pertains to behavior that can be reasonably assumed and that clients depend on.”

You should not analyze the problem in isolation. Consider the reasonable assumptions made by the users of your design. While FixedDepositAccount might seem acceptable in isolation, it fails when you ask: “What would the user expect?”

Design by Contract

This is one of my favorite topics in software engineering. Design by Contract was introduced by Bertrand Meyer in his 1988 book Object-Oriented Software Construction, and it addresses how a client should interact with your abstraction.

Meyer stated:

“A routine redeclaration [in a derivative] may only replace the original precondition by one equal or weaker and postconditions by one equal or stronger.”

In essence, when developing a base class, you must carefully consider the “ins and outs” of your methods.

Inputs (Pre-conditions)

The inputs accepted by methods in derived classes should be a subset — or more constrained version — of those accepted by the base class.

A derived class can extend the input parameters without breaking the expected behavior when these inputs are handled within the base class’s domain.

Consider this example:

class CreditCard {
    processPayment(amount: number): boolean {
        if (amount < 0) throw new Exception();
        return true;
    }
}

class VipCreditCard extends CreditCard {
    processPayment(amount: number): boolean {
        if (amount < 100) throw new Exception(); // more restrictive — violation
        return true;
    }
}
  Set of inputs <derivative-class>
  ┌──────────────────────────────────┐
  │                                  │
  │   Set of inputs <base-class>     │
  │   ┌──────────────────────┐       │
  │   │                      │       │
  │   └──────────────────────┘       │
  │                                  │
  └──────────────────────────────────┘

The VipCreditCard.processPayment method is more restrictive than the base method — it doesn’t allow additional cases. If you pass the number 5, the base class accepts it, but VipCreditCard throws. This inconsistency undermines the base modeling. If you’re using a factory method where you can’t determine which derivative is being instantiated, this becomes a significant problem.

The only way to fix it without breaking LSP is to remodel your inheritance solution.

Outputs (Post-conditions)

The post-conditions in your derived methods should be equal to or stronger than those in the base class.

class CreditCard {
    processPayment(amount: number): boolean {
        return true;
    }
}

class VipCreditCard extends CreditCard {
    processPayment(amount: number): boolean {
        return amount < 500; // weakens the output — violation
    }
}
  Set of outputs <base-class>
  ┌──────────────────────────────────┐
  │                                  │
  │   Set of outputs <derived-class> │
  │   ┌──────────────────────┐       │
  │   │                      │       │
  │   └──────────────────────┘       │
  │                                  │
  └──────────────────────────────────┘

In the base class, processPayment always returns true. The client can rely on that. But VipCreditCard introduces the possibility of false. Now the client can’t reliably predict the behavior. This mirrors the same logic as the preconditions rule.

The Connection to Agile

Currently, almost every company claims to follow agile principles — we use sprints, boards, daily meetings. But if you look closer, many of these companies aren’t truly agile. To be agile, you must stick to the core pillars outlined in the Agile Manifesto:

The domain model is a living creature. It needs to be discovered, understood, and carefully nurtured. How?

When it comes to LSP, it’s essential to understand the behavior of your abstractions. And how do you do that? By talking to the experts. Ask the right questions, collaborate with your team, get deep insights into the problem you’re solving. After all, how can you set a solid contract if you don’t fully understand the issue at hand?


To wrap up this principle, I’ll leave you with this from Uncle Bob, which I think captures everything:

“The OCP is at the heart of many of the claims made for OOD. When this principle is in effect, applications are more maintainable, reusable, and robust. The LSP is one of the prime enablers of the OCP. It is the substitutability of subtypes that allows a module, expressed in terms of a base type, to be extensible without modification. That substitutability must be something that developers can depend on implicitly. Thus, the contract of the base type has to be well and prominently understood, if not explicitly enforced, by the code.”


Share this post on:

Previous Post
SOLID — The Dependency Inversion Principle
Next Post
SOLID — The Open/Closed Principle