If you're learning clean code, you'll keep running into the "SOLID" principles. The L stands for the Liskov Substitution Principle (LSP) — a scary name for a genuinely simple idea. This guide starts from zero, so no prior knowledge is needed.
What it is
The Liskov Substitution Principle says:
If something is supposed to work like a certain type, then any "version" of that type must be able to take its place without causing problems.
That's the whole thing. If you call something a Doctor, it has to actually behave like a doctor everywhere a doctor is expected. If you call something an ApiClient, it has to behave like one everywhere. Break that, and your program fails — usually later, in a confusing way.
The principle is named after Barbara Liskov, a computer scientist who described it back in 1987. But you don't need the academic version to use it well.
First, what does "substitute" mean?
This is the word that makes the whole principle click, so let's be clear about it.
A substitute is a stand-in — something you swap in for another thing, expecting it to do the same job.
Think of a substitute teacher. When your regular teacher is away, a substitute steps in. You still expect the class to run: attendance is taken, the lesson happens, the bell rings on time. A good substitute does the teacher's job without the students even noticing a difference.
Now imagine a "substitute" who shows up, sits down, and refuses to teach. Technically a person is standing at the front of the room — but they've failed at being a substitute.
That's exactly what LSP is about. A "child" type is a substitute for its "parent" type. It must be able to step in and do the parent's job. If it can't, it's a bad substitute — and shouldn't have claimed to be one.
The idea as a diagram
Both a Surgeon and a Receptionist claim to be a kind of Doctor. But only the surgeon can actually stand in for one. The receptionist is the bad substitute — and LSP is the rule that tells you to never build one.
The simple example
In code, we have a general type (the "parent") and specific versions of it (the "children"). Any doctor should be able to treat a patient:
A Surgeon is a proper substitute for a Doctor. Ask it to treat a patient and it works.
Where it fails
Now someone makes a mistake. A receptionist works at the hospital, so they think, "let's make Receptionist a Doctor too":
Here's the trap: this code runs fine when you write it. The computer doesn't complain. It only blows up when the code is actually used:
This is the key lesson: the computer only checks that things look right — correct method names, correct spelling. It does not check that they actually behave right. That's your job. A receptionist is not a doctor, so making it one was the bug.
Failures like this show up in a few common shapes:
- Refusing to do the job. The receptionist that throws an error instead of treating a patient.
- Being pickier than promised. If a parent says "you can give me any name, even an empty one," a child is not allowed to secretly demand a non-empty name. Code that trusted the parent's rule will break.
- Giving back something different. If a parent promises to return a sorted list, a child that returns an unsorted one breaks anyone who counted on the order — even though both "return a list."
A production example: where you'll really use this
The doctor story is easy to picture, but here's where LSP earns its keep in an actual app.
Almost every app talks to a server to fetch data. You don't want your app glued to one specific way of doing that — and during testing, you want a fake version that returns pretend data instead of hitting a real server. LSP is what makes this swap safe.
Step 1 — Define the job description (the parent). This is a promise: anything calling itself an ApiClient can get and post.
Step 2 — Make two substitutes that both keep the promise.
Both are good substitutes: same inputs, same kind of result, no surprise crashes. Either one can stand in for an ApiClient.
Step 3 — The rest of your app only knows the job description, never which substitute it got.
Step 4 — Swap freely. This is the payoff.
UserRepository has no idea whether it's talking to a real server or a fake one. It doesn't need to. Because both clients are honest substitutes for ApiClient, you can switch between them freely — real data in production, fake data in tests — and nothing else in your app changes.
That's LSP quietly doing its job. And notice the danger: if MockApiClient secretly threw an error or returned the wrong shape of data, this swap would still look fine but break at runtime — the exact same receptionist mistake, just hidden inside a real feature.
A real payment example: bKash and Nagad
Here's a violation you could easily write by accident. Say you're building payments and you make a parent for every payment service. You figure all of them can send money, make payments — and handle subscription payments. So you put all three in the parent:
Now bKash is happy — it really does support subscriptions:
But Nagad doesn't offer subscription payments. Yet because the parent promised the method, Nagad is forced to have one — and the only thing it can honestly do is fail:
Now any code written for the parent gets a nasty surprise:
This is the receptionist mistake again, just wearing a different outfit. The parent promised something (subscriptionPayment) that not every child can actually deliver. Nagad is a bad substitute — not because Nagad is bad, but because we put a promise in the parent that wasn't true for everyone.
The fix: don't promise what everyone can't keep
The problem isn't Nagad — it's the parent. Move the shared abilities into the base, and pull subscriptionPayment out into a separate contract that only the gateways that truly support it sign up for:
Now subscription code asks for the right promise, and it's impossible to accidentally hand it something that can't deliver:
Notice how much better this is: the mistake is now caught while you're writing the code, not when a real user tries to subscribe. The best fix for an LSP problem is usually to stop promising something in the parent that not every child can keep.
A simple warning sign
If you ever catch yourself writing this:
…that's a red flag. If you have to check which specific version you're holding to stop things from breaking, your substitute isn't a real substitute. A proper one just works — you shouldn't have to babysit it.
An easy checklist
Before you make one thing a "child" of another, ask:
- Can it do everything the parent can do? (bKash can do subscriptions ✓. Nagad can't ✗ — so subscriptions don't belong in the shared parent.)
- Does it give back what the parent promised? (Same kind of result, same shape.)
- Will it avoid crashing where the parent wouldn't? (No surprise errors.)
- Can people use it without knowing which exact version it is?
Four "yes" answers means you've got a proper substitute. Any "no" means it probably shouldn't be a child of that type.
The takeaway
The Liskov Substitution Principle is really just about keeping promises.
When you say something is a certain type, you're promising how it behaves. Break that promise and the computer won't warn you — but your program will fail later, often in confusing ways. Keep it, and everything "just works," whether it's a surgeon standing in for a doctor, a fake client standing in for a real one, or bKash and Nagad each only promising what they can truly deliver.
So next time you make one class extend another, pause and ask: "Is this a real substitute — or am I asking Nagad to run a subscription?"
Comments
Post a Comment