- Published on
Mastering the Liskov Substitution Principle with Real-World TypeScript Examples
- π Mastering the Liskov Substitution Principle (LSP) with Real-World Code Examples
- π§ What Is the Liskov Substitution Principle?
- β Bad Example: Violating LSP
- β Good Example: Applying LSP Correctly
- π How to Achieve the Liskov Substitution Principle
- π§ How to Detect LSP Violations
- π Benefits of Applying LSP
- π₯ Advanced Example: HTTP Request Handlers
- β Good Code: Respecting LSP
- π― Key Takeaways from This Example
- π§ Advanced Problem: Shape Area Calculator
- π₯ Advanced Example: HTTP Request Handlers
- β Good Code: Respecting LSP
- π― Key Takeaways from This Example
- π§° Pro Tips
- π₯ Advanced Example: HTTP Request Handlers
- β Good Code: Respecting LSP
- π― Key Takeaways from This Example
- π Summary
π Mastering the Liskov Substitution Principle (LSP) with Real-World Code Examples
The Liskov Substitution Principle (LSP) is the L in SOLIDβa set of principles for writing clean, maintainable, and extendable object-oriented code.
π§ What Is the Liskov Substitution Principle?
Definition: If class B is a subclass of class A, then objects of type A may be replaced with objects of type B without altering the correctness of the program.
In other words, a child class must behave in such a way that it doesn't break the functionality or expectations of its parent class.
β Bad Example: Violating LSP
Scenario:
You have a class Bird
with a method fly()
, and you create a Penguin
class as a subtype. But penguins can't fly!
class Bird {
fly() {
console.log("Flying high in the sky!");
}
}
class Sparrow extends Bird {}
class Penguin extends Bird {
fly() {
throw new Error("Penguins can't fly!");
}
}
function letTheBirdFly(bird: Bird) {
bird.fly();
}
const penguin = new Penguin();
letTheBirdFly(penguin); // β Breaks the system!
𧨠Problem:
- The client code expects all
Bird
instances to fly, butPenguin
breaks the contract. - This violates LSP and can cause runtime errors, making the system brittle and hard to test.
β Good Example: Applying LSP Correctly
Solution:
Refactor by introducing an abstraction that respects capabilities.
abstract class Bird {}
interface FlyingBird {
fly(): void;
}
class Sparrow extends Bird implements FlyingBird {
fly() {
console.log("Sparrow flying!");
}
}
class Penguin extends Bird {
swim() {
console.log("Penguin swimming!");
}
}
function letTheBirdFly(bird: FlyingBird) {
bird.fly();
}
const sparrow = new Sparrow();
letTheBirdFly(sparrow); // β
Works perfectly
const penguin = new Penguin();
// letTheBirdFly(penguin); β Not allowed at compile time
π How to Achieve the Liskov Substitution Principle
- Use interfaces or abstract classes to define clear contracts.
- Ensure subclasses fulfill the contract of the base class without weakening preconditions or strengthening postconditions.
- Don't override methods to throw exceptions for expected behavior.
- Design hierarchies around behavior, not taxonomy. (A penguin is a bird, but not a flying bird.)
π§ How to Detect LSP Violations
Ask yourself:
- β Does the subclass override a method to throw exceptions or skip behavior?
- β Do you have
if instanceof
checks in client code? - β Does a subclass break assumptions of the superclass?
- β Are your unit tests failing when a subclass replaces the parent?
If yes to any, you're likely violating LSP.
π Benefits of Applying LSP
Benefit | Explanation |
---|---|
π Polymorphism | Allows safe substitution of classes without changing behavior |
β Predictability | Reduces bugs caused by unexpected subclass behavior |
π§ͺ Testability | Easier unit testing and mocking |
βοΈ Maintainability | Easier to add new behavior without breaking existing code |
π Scalability | Promotes extensible system architecture |
π₯ Advanced Example: HTTP Request Handlers
β Bad Code: Violating LSP
You have a RequestHandler
base class, and you extend it for different request types like AuthenticatedRequestHandler
. But the subclass throws errors if the request isn't authenticated, breaking expectations.
class Request {
constructor(public headers: Record<string, string>) {}
}
class RequestHandler {
handle(req: Request): string {
return "Request handled";
}
}
class AuthenticatedRequestHandler extends RequestHandler {
handle(req: Request): string {
if (!req.headers["Authorization"]) {
throw new Error("Unauthorized request"); // β Violates LSP
}
return "Authenticated request handled";
}
}
function processRequest(handler: RequestHandler, req: Request) {
console.log(handler.handle(req));
}
const req1 = new Request({});
const handler1 = new AuthenticatedRequestHandler();
processRequest(handler1, req1); // β Throws unexpected error
π¨ Problem:
processRequest
expects anyRequestHandler
to handle a request safely.- But
AuthenticatedRequestHandler
changes the contractβit throws an error instead. - This breaks the substitutability, violating LSP.
β Good Code: Respecting LSP
β Solution: Separate concerns using interfaces or strategy pattern
class Request {
constructor(public headers: Record<string, string>) {}
}
interface IRequestHandler {
handle(req: Request): string;
}
class PublicRequestHandler implements IRequestHandler {
handle(req: Request): string {
return "Public request handled";
}
}
class AuthenticatedRequestHandler implements IRequestHandler {
handle(req: Request): string {
const token = req.headers["Authorization"];
return token
? "Authenticated request handled"
: "Guest access: limited features";
}
}
function processRequest(handler: IRequestHandler, req: Request) {
console.log(handler.handle(req));
}
const req1 = new Request({});
const req2 = new Request({ Authorization: "Bearer token" });
processRequest(new PublicRequestHandler(), req1); // β
Public request handled
processRequest(new AuthenticatedRequestHandler(), req1); // β
Guest access: limited features
processRequest(new AuthenticatedRequestHandler(), req2); // β
Authenticated request handled
π― Key Takeaways from This Example
Principle | Explanation |
---|---|
π LSP | Any IRequestHandler can be used in place of another without surprising behavior |
π‘οΈ Safety | No unexpected exceptions or broken flow |
π Detect Violation | If a subclass strengthens preconditions (like requiring headers), it's a sign of LSP violation |
π Fix | Create clearer contracts or segregate responsibilities using interfaces |
This pattern is very useful in middleware systems, API routers, or request pipelines where different handlers must conform to the same interface and expectations.
π§ Advanced Problem: Shape Area Calculator
β Bad Example (Violating LSP)
class Rectangle {
constructor(public width: number, public height: number) {}
setWidth(width: number) {
this.width = width;
}
setHeight(height: number) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width: number) {
this.width = width;
this.height = width;
}
setHeight(height: number) {
this.width = height;
this.height = height;
}
}
function resize(rect: Rectangle) {
rect.setWidth(5);
rect.setHeight(4);
console.log(rect.getArea()); // Expected 20
}
resize(new Rectangle(2, 3)); // β
20
resize(new Square(2, 2)); // β 16
β Good Example (Respecting LSP)
interface Shape {
getArea(): number;
}
class Rectangle implements Shape {
constructor(public width: number, public height: number) {}
getArea() {
return this.width * this.height;
}
}
class Square implements Shape {
constructor(public size: number) {}
getArea() {
return this.size * this.size;
}
}
function printArea(shape: Shape) {
console.log(shape.getArea());
}
printArea(new Rectangle(5, 4)); // β
20
printArea(new Square(4)); // β
16
Absolutely! Here's one more advanced Liskov Substitution Principle (LSP) violation example from the programming world, especially relevant for API request handling in backend/frontend systems (e.g., TypeScript or Node.js).
π₯ Advanced Example: HTTP Request Handlers
β Bad Code: Violating LSP
You have a RequestHandler
base class, and you extend it for different request types like AuthenticatedRequestHandler
. But the subclass throws errors if the request isn't authenticated, breaking expectations.
class Request {
constructor(public headers: Record<string, string>) {}
}
class RequestHandler {
handle(req: Request): string {
return "Request handled";
}
}
class AuthenticatedRequestHandler extends RequestHandler {
handle(req: Request): string {
if (!req.headers["Authorization"]) {
throw new Error("Unauthorized request"); // β Violates LSP
}
return "Authenticated request handled";
}
}
function processRequest(handler: RequestHandler, req: Request) {
console.log(handler.handle(req));
}
const req1 = new Request({});
const handler1 = new AuthenticatedRequestHandler();
processRequest(handler1, req1); // β Throws unexpected error
π¨ Problem:
processRequest
expects anyRequestHandler
to handle a request safely.- But
AuthenticatedRequestHandler
changes the contractβit throws an error instead. - This breaks the substitutability, violating LSP.
β Good Code: Respecting LSP
β Solution: Separate concerns using interfaces or strategy pattern
class Request {
constructor(public headers: Record<string, string>) {}
}
interface IRequestHandler {
handle(req: Request): string;
}
class PublicRequestHandler implements IRequestHandler {
handle(req: Request): string {
return "Public request handled";
}
}
class AuthenticatedRequestHandler implements IRequestHandler {
handle(req: Request): string {
const token = req.headers["Authorization"];
return token
? "Authenticated request handled"
: "Guest access: limited features";
}
}
function processRequest(handler: IRequestHandler, req: Request) {
console.log(handler.handle(req));
}
const req1 = new Request({});
const req2 = new Request({ Authorization: "Bearer token" });
processRequest(new PublicRequestHandler(), req1); // β
Public request handled
processRequest(new AuthenticatedRequestHandler(), req1); // β
Guest access: limited features
processRequest(new AuthenticatedRequestHandler(), req2); // β
Authenticated request handled
π― Key Takeaways from This Example
Principle | Explanation |
---|---|
π LSP | Any IRequestHandler can be used in place of another without surprising behavior |
π‘οΈ Safety | No unexpected exceptions or broken flow |
π Detect Violation | If a subclass strengthens preconditions (like requiring headers), it's a sign of LSP violation |
π Fix | Create clearer contracts or segregate responsibilities using interfaces |
This pattern is very useful in middleware systems, API routers, or request pipelines where different handlers must conform to the same interface and expectations.
Would you like to turn all examples into a downloadable cheat sheet or a printable PDF for reference?
π§° Pro Tips
- Use composition over inheritance when behaviors don't fit a strict βis-aβ relationship.
- Write unit tests that pass parent classes into your system. Then try subclasses and see if expectations are met.
- Avoid subclassing for the sake of DRY (Don't Repeat Yourself); behavior compatibility matters more.
Absolutely! Here's one more advanced Liskov Substitution Principle (LSP) violation example from the programming world, especially relevant for API request handling in backend/frontend systems (e.g., TypeScript or Node.js).
π₯ Advanced Example: HTTP Request Handlers
β Bad Code: Violating LSP
You have a RequestHandler
base class, and you extend it for different request types like AuthenticatedRequestHandler
. But the subclass throws errors if the request isn't authenticated, breaking expectations.
class Request {
constructor(public headers: Record<string, string>) {}
}
class RequestHandler {
handle(req: Request): string {
return "Request handled";
}
}
class AuthenticatedRequestHandler extends RequestHandler {
handle(req: Request): string {
if (!req.headers["Authorization"]) {
throw new Error("Unauthorized request"); // β Violates LSP
}
return "Authenticated request handled";
}
}
function processRequest(handler: RequestHandler, req: Request) {
console.log(handler.handle(req));
}
const req1 = new Request({});
const handler1 = new AuthenticatedRequestHandler();
processRequest(handler1, req1); // β Throws unexpected error
π¨ Problem:
processRequest
expects anyRequestHandler
to handle a request safely.- But
AuthenticatedRequestHandler
changes the contractβit throws an error instead. - This breaks the substitutability, violating LSP.
β Good Code: Respecting LSP
β Solution: Separate concerns using interfaces or strategy pattern
class Request {
constructor(public headers: Record<string, string>) {}
}
interface IRequestHandler {
handle(req: Request): string;
}
class PublicRequestHandler implements IRequestHandler {
handle(req: Request): string {
return "Public request handled";
}
}
class AuthenticatedRequestHandler implements IRequestHandler {
handle(req: Request): string {
const token = req.headers["Authorization"];
return token
? "Authenticated request handled"
: "Guest access: limited features";
}
}
function processRequest(handler: IRequestHandler, req: Request) {
console.log(handler.handle(req));
}
const req1 = new Request({});
const req2 = new Request({ Authorization: "Bearer token" });
processRequest(new PublicRequestHandler(), req1); // β
Public request handled
processRequest(new AuthenticatedRequestHandler(), req1); // β
Guest access: limited features
processRequest(new AuthenticatedRequestHandler(), req2); // β
Authenticated request handled
π― Key Takeaways from This Example
Principle | Explanation |
---|---|
π LSP | Any IRequestHandler can be used in place of another without surprising behavior |
π‘οΈ Safety | No unexpected exceptions or broken flow |
π Detect Violation | If a subclass strengthens preconditions (like requiring headers), it's a sign of LSP violation |
π Fix | Create clearer contracts or segregate responsibilities using interfaces |
This pattern is very useful in middleware systems, API routers, or request pipelines where different handlers must conform to the same interface and expectations.
Would you like to turn all examples into a downloadable cheat sheet or a printable PDF for reference?
π Summary
Topic | Notes |
---|---|
β What is LSP | Subclasses should be replaceable for parent classes without altering program behavior. |
β Bad Example | Penguin flying or Square as a Rectangle violates LSP |
β Good Design | Use composition, interfaces, and behavior-centric design |
π Detection | Watch for exceptions, if-checks, or broken assumptions in subclasses |
π Fixes | Refactor using interfaces, split behaviors, rethink hierarchies |