Liskov Substitution Principle (LSP) in Software Development: Simplified (Explaining “L” in SOLID principles)
Certainly. Let’s explore the Liskov Substitution Principle (LSP) with an example related to a food delivery application like Zomato. The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
Here’s an example demonstrating the Liskov Substitution Principle in the context of Zomato:
```python
from abc import ABC, abstractmethod
# Base class
class Restaurant(ABC):
def __init__(self, name, cuisine):
self.name = name
self.cuisine = cuisine
@abstractmethod
def get_delivery_time(self) -> int:
pass
@abstractmethod
def calculate_bill(self, items) -> float:
pass
# Subclass
class StandardRestaurant(Restaurant):
def get_delivery_time(self) -> int:
# Assume standard delivery time is 30 minutes
return 30
def calculate_bill(self, items) -> float:
return sum(item.price for item in items)
# Another subclass
class PremiumRestaurant(Restaurant):
def get_delivery_time(self) -> int:
# Premium restaurants have faster delivery
return 20
def calculate_bill(self, items) -> float:
# Premium restaurants have a 10% service charge
subtotal = sum(item.price for item in items)
return subtotal * 1.10
# A function that uses Restaurant objects
def estimate_delivery(restaurant: Restaurant, items) -> str:
delivery_time = restaurant.get_delivery_time()
total_bill = restaurant.calculate_bill(items)
return f”Your order from {restaurant.name} will be delivered in {delivery_time} minutes. Total bill: ${total_bill:.2f}”
# Usage
standard_restaurant = StandardRestaurant(“Tasty Bites”, “Indian”)
premium_restaurant = PremiumRestaurant(“Gourmet Delights”, “French”)
order_items = [Item(“Dish1”, 10), Item(“Dish2”, 15)]
print(estimate_delivery(standard_restaurant, order_items))
print(estimate_delivery(premium_restaurant, order_items))
```
In this example:
1. We have a base `Restaurant` class with abstract methods `get_delivery_time()` and `calculate_bill()`.
2. We have two subclasses: `StandardRestaurant` and `PremiumRestaurant`, each implementing these methods differently.
3. The `estimate_delivery()` function takes a `Restaurant` object and works with it without knowing or caring about its specific subtype.
This adheres to the Liskov Substitution Principle because:
- Both `StandardRestaurant` and `PremiumRestaurant` can be used wherever a `Restaurant` is expected.
- The `estimate_delivery()` function works correctly regardless of which subtype of `Restaurant` is passed to it.
- The subclasses don’t change the expected behavior of the base class methods. They may enhance it (like adding a service charge), but they don’t fundamentally alter how the methods work.
If we were to violate LSP, it might look something like this:
```python
class FreeDeliveryRestaurant(Restaurant):
def get_delivery_time(self) -> int:
return 45 # Longer delivery time for free delivery
def calculate_bill(self, items) -> str:
# Violates LSP by changing return type
return “Free delivery! Please pay at the restaurant.”
```
This violates LSP because the `calculate_bill()` method changes the return type from `float` to `str`, which would break any code expecting a numeric value.
By adhering to the Liskov Substitution Principle:
1. We can easily add new types of restaurants without breaking existing code.
2. The system becomes more flexible and extensible.
3. We can use polymorphism effectively, treating all restaurants uniformly in functions like `estimate_delivery()`.
4. It encourages us to design our class hierarchies carefully, ensuring that subclasses truly are specializations of their base classes.
This principle is crucial in a system like Zomato, where you might have various types of restaurants, each with their own specific behaviors, but all needing to fit into the same overall system for ordering and delivery.
*Real-World Use Case: Ola (Ride-Sharing App)**
Imagine you are developing a ride-sharing app like Ola. Here are some of the main features you need to handle:
1. **Ride Types:** Different types of rides (e.g., Mini, Sedan, Prime).
2. **Calculating Fare:** Each ride type may have different fare calculations.
3. **Booking Rides:** Users can book rides of various types.
**Liskov Substitution Principle in Ola Software:**
1. **Base Class:**
— Define a base class `Ride` that provides a common interface for all ride types.
```python
class Ride:
def calculate_fare(self, distance):
raise NotImplementedError(“Subclasses should implement this method.”)
def book(self, user, destination):
raise NotImplementedError(“Subclasses should implement this method.”)
```
2. **Subclass for Each Ride Type:**
— Create subclasses for each ride type, ensuring they implement the methods of the base class `Ride`.
```python
class MiniRide(Ride):
def calculate_fare(self, distance):
return distance * 10 # Example fare calculation for Mini
def book(self, user, destination):
# Logic to book a Mini ride
print(f”Mini ride booked for {user} to {destination}.”)
class SedanRide(Ride):
def calculate_fare(self, distance):
return distance * 15 # Example fare calculation for Sedan
def book(self, user, destination):
# Logic to book a Sedan ride
print(f”Sedan ride booked for {user} to {destination}.”)
class PrimeRide(Ride):
def calculate_fare(self, distance):
return distance * 20 # Example fare calculation for Prime
def book(self, user, destination):
# Logic to book a Prime ride
print(f”Prime ride booked for {user} to {destination}.”)
```
3. **Using the Liskov Substitution Principle:**
— Ensure that instances of the subclasses can be used interchangeably with the base class `Ride`.
```python
def book_ride(ride: Ride, user, destination):
ride.book(user, destination)
fare = ride.calculate_fare(10) # Assuming distance is 10 units
print(f”Fare: {fare}”)
# Example usage
user = “John Doe”
destination = “Airport”
mini_ride = MiniRide()
sedan_ride = SedanRide()
prime_ride = PrimeRide()
book_ride(mini_ride, user, destination)
book_ride(sedan_ride, user, destination)
book_ride(prime_ride, user, destination)
```
**Why Liskov Substitution Principle is Important:**
- **Reliability:** Substituting a superclass with a subclass should not cause the program to break or behave unexpectedly.
- **Flexibility:** The system is more flexible as new subclasses can be introduced without changing the existing code.
- **Maintainability:** The code is easier to maintain and extend since each subclass adheres to the contract defined by the base class.
**Analogy:**
Think of LSP like having different types of vehicles (e.g., car, truck, bike) that can all drive on the same road. Each vehicle can follow the same basic rules of the road (e.g., stop at red lights, go on green lights) without needing special exceptions. Similarly, in software, subclasses should follow the same rules as their superclass, allowing them to be used interchangeably.
By following the Liskov Substitution Principle in your Ola ride-sharing app, you ensure that different ride types (Mini, Sedan, Prime) can be used seamlessly in place of one another, maintaining consistent behavior and functionality throughout the application.