Deep Dive Into Liskov Substitution Principle
History With Barbara Liskov
Liskov Substitution Principle (LSP), introduced by Barbara Liskov in a 1987 conference keynote, is the third principle in the SOLID acronym, which represents five principles of object-oriented programming and design. The principle's formal statement is:
"If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program."
In simpler words, derived classes must be substitutable for their base classes. That is, a program that uses a base class must be able to substitute a subclass without affecting the correctness of the program.
Application In OOP
Robert C Martin, better known as Uncle Bob, is a significant figure in the software engineering world. While he didn't invent the Liskov Substitution Principle (that was Barbara Liskov), he's perhaps best known for popularizing the SOLID principles, including Liskov's principle, in the early 2000s.


Time To Transition From JavaScript To TypeScript
Level Up Your TypeScript And Object Oriented Programming Skills. The only complete TypeScript course on the marketplace you building TypeScript apps like a PRO.
SEE COURSE DETAILSThe Principle
This principle is based on behavioral subtyping, which is a fundamental principle in type theory, the mathematical study of data and its manipulation. It ensures that a subtype can always be substituted in place of its supertype, preserving program correctness.
Let's understand it with a TypeScript example. Consider a base class Bird
:
class Bird {
fly(): void {
// Implementations of bird flying...
}
}
Now, you create a subclass Penguin
:
class Penguin extends Bird {
fly(): void {
throw new Error("Penguins can't fly!");
}
}
This violates the Liskov Substitution Principle because you can't substitute
Penguin
forBird
without causing potential problems. If a function expects an instance ofBird
and calls thefly
method, but receives aPenguin
instead, an error will occur.
To comply with the LSP, we might refactor the code like this:
class Bird {
fly(): void {
// Implementations of bird flying...
}
}
class FlightlessBird extends Bird {
fly(): void {
// Maybe log a message or do nothing at all, but don't throw an error
}
}
class Penguin extends FlightlessBird {
// Penguin-specific methods...
}
In this new setup, a FlightlessBird
is still a Bird
and can respond to
the fly
method, but doesn't take action because it can't fly. This way, any
function expecting a Bird
can still work with a Penguin
or FlightlessBird
without problems.
Ultimately, following the Liskov Substitution Principle helps in maintaining the reusability and interchangeability of subclasses, making software design more robust and less prone to errors due to incorrect use of a subtype.
Real World Use Case
Let's consider a real world example - say, an online payment processing system where you have multiple methods to process payments like CreditCard, DebitCard, and PayPal.
First, we'll establish a base class, PaymentProcessor
, and a method that any
form of payment needs to implement:
abstract class PaymentProcessor {
abstract processPayment(amount: number): void;
}
Now, we create the specific payment method classes:
class CreditCardProcessor extends PaymentProcessor {
processPayment(amount: number): void {
console.log(`Processing credit card payment of $${amount}`);
// implementation details for processing credit card payment...
}
}
class DebitCardProcessor extends PaymentProcessor {
processPayment(amount: number): void {
console.log(`Processing debit card payment of $${amount}`);
// implementation details for processing debit card payment...
}
}
class PayPalProcessor extends PaymentProcessor {
processPayment(amount: number): void {
console.log(`Processing PayPal payment of $${amount}`);
// implementation details for processing PayPal payment...
}
}
In this scenario, every payment processor class is a subtype
of PaymentProcessor
and each of them implements the processPayment
method.
Now, let's create a function that takes an instance of PaymentProcessor
and
uses it to process a payment. Because of the Liskov Substitution Principle, this
function can use any subtype of PaymentProcessor
:
function executePayment(paymentProcessor: PaymentProcessor, amount: number) {
paymentProcessor.processPayment(amount);
}
// Now, we can process payments using any of the payment methods:
const creditCardProcessor = new CreditCardProcessor();
executePayment(creditCardProcessor, 100);
const debitCardProcessor = new DebitCardProcessor();
executePayment(debitCardProcessor, 200);
const payPalProcessor = new PayPalProcessor();
executePayment(payPalProcessor, 300);
This design respects the Liskov Substitution Principle, as
any PaymentProcessor
subtype can be used in the executePayment
function
without causing issues. Each processor has its own specific implementation
of processPayment
, but the way they're used doesn't need to change based on
the specific subtype.
What Can You Do Next 🙏😊
If you liked the article, consider subscribing to Cloudaffle, my YouTube Channel, where I keep posting in-depth tutorials and all edutainment stuff for software developers.