Skip to main content

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.

TypeScript Course Instructor Image
TypeScript Course Instructor Image

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 DETAILS

The 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 for Bird without causing potential problems. If a function expects an instance of Bird and calls the fly method, but receives a Penguin 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.

YouTube @cloudaffle