SOLID Design Principles are a set of five design principles introduced by Robert C. Martin. These principles help developers build maintainable, flexible and scalable software systems.
SOLID is an acronym for the following design principles:
- Single Responsibility Principle.
- Open-Closed Principle.
- Liskov Substitution Principle.
- Interface Segregation Principle.
- Dependency Inversion Principle.
Single Responsibility Principle(SRP):
SRP says that a class should have only one responsibility and should have only one reason to change.
For instance, we have a class called 'Order' that represents a customer's order at a restaurant. The Order class has two responsibilities: handling the order items and printing the receipt.
class Order:
def __init__(self, order_items):
self.order_items = order_items
def calculate_total(self):
# logic to calculate total cost of order
pass
def print_receipt(self):
# logic to print receipt
pass
The above implementation violates SRP since it has two responsibilities. We can separate these responsibilities into two classes.
class Order:
def __init__(self, order_items):
self.order_items = order_items
def calculate_total(self):
# logic to calculate total cost of order
pass
class ReceiptPrinter:
def print_receipt(self, order):
# logic to print receipt
pass
The 'Order' class is now responsible for handling the order items, and the 'ReceiptPrinter' class is responsible only for printing the receipt. This separation makes it easier to update or replace the receipt printing logic in the future without affecting the 'Order' class.
Open-Closed Principle(OCP):
The OCP states "Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification".
Suppose we have an existing 'Product' class that represents a product in the store, with properties such as 'name', 'price' and 'description'.
class Product:
def __init__(self, name, price, description):
self.name = name
self.price = price
self.description = description
def get_price(self):
return self.price
Now suppose we were to add a new feature where we offer discounts on certain products. We can apply OCP here by creating a new 'DiscountProduct' subclass that inherits 'Product' class without modifying the 'Product' class.
class Product:
def __init__(self, name, price, description):
self.name = name
self.price = price
self.description = description
def get_price(self):
return self.price
class DiscountedProduct(Product):
def __init__(self, name, price, description, discount):
super().__init__(name, price, description)
self.discount = discount
def get_price(self):
discounted_price = super().get_price() - self.discount
return max(discounted_price, 0)
The 'DiscountedProduct' class overrides the 'get_price' method of the Product class to return the discounted price of the product.
# Usage
product = Product("Shirt", 20, "A basic shirt")
print("Product price:", product.get_price())
discounted_product = DiscountedProduct("Jacket", 100, "A warm jacket", 25)
print("Discounted product price:", discounted_product.get_price())
By applying the OCP in the above example, we have made our code more maintainable and flexible. If we want to add new features in the future, we can create new subclasses of 'Product' without modifying existing code. This makes our code more scalable and less prone to errors.
Liskov substitution principle(LSP):
Liskov Substitution Principle states that "Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program."
Let’s take an example of class 'Car' that would indicate the type of a car. The 'PetrolCar' class inherits the 'Car' class.
class Car:
def __init__(self, type):
self.type = type
class PetrolCar(Car):
def __init__(self, type):
self.type = type
car = Car('SUV'):
car.properties = {'color': 'Red', 'gear': 'Auto', 'capacity': 6}
print(car.properties)
petrol_car = PetrolCar('Sedan')
petrol_car.properties = ('Blue', 'Manual', 4)
print(petrol_car.properties)
Output:
{'color': 'Red', 'gear': 'Auto', 'capacity': 6}
('Blue', 'Manual', 4)
In the above example, adding car properties does not follow any standard and it is upto the developers to implement it as a dictionary or tuple or other ways.
Now, if we want to find all the red-coloured cars based on the current implementation.
cars = [car, petrol_car]
def find_red_cars(cars):
red_cars = 0
for car in cars:
if car.properties['color'] == 'Red':
red_cars += 1
print(f'Number of Red Cars = {red_cars}')
find_red_cars(cars)
# TypeError: tuple indices must be integers or slices, not str
Here, the implementation breaks the LSP as we cannot replace Car's object with its subclass PetrolCar's object in the function written to find red-colored cars.
So how can we fix this?, we can implement getters and setters methods in the Car's class using which we can set and get the properties of the Car without leaving the decision to developers on how to implement Car's properties.
class Car:
def __init__(self, type):
self.type = type
self.car_properties = {}
def set_properties(self, color, gear, capacity):
self.car_properties = {'color': color, 'gear': gear, 'capacity': capacity}
def get_properties(self):
return self.car_properties
class PetrolCar(Car):
def __init__(self, type):
self.type = type
self.car_properties = {}
car = Car('SUV'):
car.set_properties('Red', 'Auto', 6)
petrol_car = PetrolCar('Sedan')
petrol_car.set_properties('Blue', 'Manual', 4)
cars = [car, petrol_car]
def find_red_cars(cars):
red_cars = 0
for car in cars:
if car.get_properties()['color'] == 'Red':
red_cars += 1
print(f'Number of Red Cars = {red_cars}')
find_red_cars(cars)
Output: Number of Red Cars = 1
Interface Segregation Principle(ISP):
The ISP states that clients should not be forced to depend on methods they do not use. In other words, it is better to have multiple small interfaces, each serving a specific purpose, rather than a single large interface with many methods.
Suppose we have a Communicator interface that includes methods for making calls, sending messages, and browsing the internet.
Now, if we want to implement a Landline phone which is a communication device, we create a new class LandlinePhone using the same CommunicationDevice interface. This is exactly when we face the problem due to a large CommunicationDevice interface we created. In the class LanlinePhone, we implement the make_calls() method, but as we also inherit abstract methods send_sms() and browse_internet() we have to provide an implementation of these two abstract methods also in the LandlinePhone class even if these are not applicable to this class LandlinePhone. We can either throw an exception or just write pass in the implementation, but we still need to provide an implementation.
from abc import ABC, abstractmethod
class Communicator(ABC):
@abstractmethod
def make_call(self, number):
pass
@abstractmethod
def send_message(self, number, message):
pass
@abstractmethod
def browse_internet(self):
pass
class SmartPhone(CommunicationDevice):
def make_calls(self, number):
#implementation
pass
def send_sms(self, number, message):
#implementation
pass
def browse_internet(self):
#implementation
pass
class LandlinePhone(CommunicationDevice):
def make_calls(self, number):
#implementation
pass
def send_sms(self, number, message):
#just pass or raise exception as this feature is not supported
pass
def browse_internet(self):
#just pass or raise exception as this feature is not supported
pass
Instead of creating a large interface, we can create a smaller interfaces for each method.
from abc import ABC, abstractmethod
class CallingDevice:
@abstractmethod
def make_calls(self, number):
pass
class MessagingDevice:
@abstractmethod
def send_sms(self, number, message):
pass
class InternetbrowsingDevice:
@abstractmethod
def browse_internet(self):
pass
class SmartPhone(CallingDevice, MessagingDevice, InternetbrowsingDevice):
def make_calls(self, number):
#implementation
pass
def send_sms(self, number, message):
#implementation
pass
def browse_internet(self):
#implementation
pass
class LandlinePhone(CallingDevice):
def make_calls(self, number):
#implementation
pass
Dependency Inversion Principle(DIP):
The Dependency Inversion Principle states that:
High level module should not depend on low level modules. Both should depend on abstractions
Abstractions should not depend on details. Details should depend on abstractions.
class LightSwitch:
def __init__(self, light):
self.light = light
def turn_on(self):
self.light.turn_on()
def turn_off(self):
self.light.turn_off()
class Light:
def turn_on(self):
# Code to turn on the light
def turn_off(self):
# Code to turn off the light
In this example, the LightSwitch class depends on the Light class to turn the light on and off. However, this violates the DIP, since the LightSwitch is a high-level module and Light is a low-level module.
To fix this, we can invert the dependency by introducing abstraction.
from abc import ABC, abstractmethod
class Switchable(ABC):
@abstractmethod
def turn_on(self):
pass
@abstractmethod
def turn_off(self):
pass
class LightSwitch:
def __init__(self, switchable):
self.switchable = switchable
def turn_on(self):
self.switchable.turn_on()
def turn_off(self):
self.switchable.turn_off()
class Light(Switchable):
def turn_on(self):
# Code to turn on the light
def turn_off(self):
# Code to turn off the light
Now, the 'LightSwitch' class relies on the 'Switchable' interface rather than the 'Light' class directly. This ensures that the high-level module ('LightSwitch') depends on an abstraction('Switchable'), rather than low-level module('Light') directly.
Top comments (0)