Examples and Non-Examples of Liskov Substitution Principle
Liskov Substitution Principle (LSP)
Examples and Non-Examples
Good Example Here is a code snippet where LSP is properly implemented:
- Java
- JavaScript
- Python
class Bird {
public void fly() {
System.out.println("Flying");
}
}
class Sparrow extends Bird {
// Sparrow inherits fly method and doesn't alter the behavior
}
public class Main {
public static void main(String[] args) {
Bird bird = new Sparrow();
bird.fly(); // Outputs "Flying"
}
}
class Bird {
fly() {
console.log('Flying')
}
}
class Sparrow extends Bird {
// Sparrow inherits fly method and doesn't alter the behavior
}
const bird = new Sparrow()
bird.fly() // Outputs "Flying"
class Bird:
def fly(self):
print("Flying")
class Sparrow(Bird):
# Sparrow inherits fly method and doesn't alter the behavior
pass
bird = Sparrow()
bird.fly() # Outputs "Flying"
Violation Example Here is a code snippet that violates LSP and an explanation of why it is a violation:
- Java
- JavaScript
- Python
class Bird {
public void fly() {
System.out.println("Flying");
}
}
class Ostrich extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Ostriches can't fly");
}
}
public class Main {
public static void main(String[] args) {
Bird bird = new Ostrich();
bird.fly(); // Throws UnsupportedOperationException
}
}
class Bird {
fly() {
console.log('Flying')
}
}
class Ostrich extends Bird {
fly() {
throw new Error("Ostriches can't fly")
}
}
const bird = new Ostrich()
bird.fly() // Throws Error: Ostriches can't fly
class Bird:
def fly(self):
print("Flying")
class Ostrich(Bird):
def fly(self):
raise NotImplementedError("Ostriches can't fly")
bird = Ostrich()
bird.fly() # Raises NotImplementedError: Ostriches can't fly
The above examples violate LSP because the subclass Ostrich
does not adhere to
the expected behavior of the superclass Bird
. An Ostrich
cannot be
substituted for a Bird
without altering the program's correctness.
Refactored Example To adhere to LSP, we can refactor the code by introducing a new hierarchy:
- Java
- JavaScript
- Python
abstract class Bird {
public abstract void move();
}
class FlyingBird extends Bird {
@Override
public void move() {
fly();
}
public void fly() {
System.out.println("Flying");
}
}
class NonFlyingBird extends Bird {
@Override
public void move() {
walk();
}
public void walk() {
System.out.println("Walking");
}
}
class Sparrow extends FlyingBird {
// Sparrow inherits fly method and move behavior
}
class Ostrich extends NonFlyingBird {
// Ostrich inherits walk method and move behavior
}
public class Main {
public static void main(String[] args) {
Bird sparrow = new Sparrow();
Bird ostrich = new Ostrich();
sparrow.move(); // Outputs "Flying"
ostrich.move(); // Outputs "Walking"
}
}
class Bird {
move() {
throw new Error('This method should be overridden')
}
}
class FlyingBird extends Bird {
move() {
this.fly()
}
fly() {
console.log('Flying')
}
}
class NonFlyingBird extends Bird {
move() {
this.walk()
}
walk() {
console.log('Walking')
}
}
class Sparrow extends FlyingBird {
// Sparrow inherits fly method and move behavior
}
class Ostrich extends NonFlyingBird {
// Ostrich inherits walk method and move behavior
}
const sparrow = new Sparrow()
const ostrich = new Ostrich()
sparrow.move() // Outputs "Flying"
ostrich.move() // Outputs "Walking"
class Bird:
def move(self):
raise NotImplementedError("This method should be overridden")
class FlyingBird(Bird):
def move(self):
self.fly()
def fly(self):
print("Flying")
class NonFlyingBird(Bird):
def move(self):
self.walk()
def walk(self):
print("Walking")
class Sparrow(FlyingBird):
# Sparrow inherits fly method and move behavior
pass
class Ostrich(NonFlyingBird):
# Ostrich inherits walk method and move behavior
pass
sparrow = Sparrow()
ostrich = Ostrich()
sparrow.move() # Outputs "Flying"
ostrich.move() # Outputs "Walking"
Real-World Scenario Consider a payment processing system in an e-commerce
platform. You might have a base class PaymentMethod
with subclasses
CreditCard
, PayPal
, and BankTransfer
. Each payment method should implement
a processPayment
method that adheres to the contract defined by the
PaymentMethod
class.
By adhering to LSP, you can ensure that new payment methods can be added without
altering the existing code. For instance, adding a new BitcoinPayment
method
should not require changes to the code that processes payments; it should
seamlessly integrate with the existing system.
By understanding and applying these examples, developers can see the practical implications of LSP and how it ensures reliable and maintainable code.