Python SOLID Principles
The SOLID principles are a set of five design principles that help developers create maintainable, scalable, and robust software. These principles are essential for writing clean and efficient object-oriented code in Python.
What are SOLID Principles?
The term SOLID is an acronym that represents the five fundamental principles of object-oriented programming:
Principle | Description |
---|---|
S – Single Responsibility Principle (SRP) | A class should have only one reason to change, meaning it should have only one job or responsibility. |
O – Open/Closed Principle (OCP) | Software entities (classes, modules, functions) should be open for extension but closed for modification. |
L – Liskov Substitution Principle (LSP) | Derived classes should be substitutable for their base classes without altering the correctness of the program. |
I – Interface Segregation Principle (ISP) | Clients should not be forced to depend on interfaces they do not use. |
D – Dependency Inversion Principle (DIP) | High-level modules should not depend on low-level modules. Instead, both should depend on abstractions. |
Examples
1. Single Responsibility Principle (SRP)
The Single Responsibility Principle states that a class should have only one reason to change. This means that a class should have a single responsibility or function.
In this example, we separate the responsibilities of handling user data and saving it to a file. This makes the code more modular and easier to maintain.
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def get_user_info(self):
return f"User: {self.name}, Email: {self.email}"
class UserDataSaver:
def save_to_file(self, user):
with open("user_data.txt", "w") as file:
file.write(user.get_user_info())
# Creating a user instance
user = User("Alice", "alice@example.com")
# Saving user data
saver = UserDataSaver()
saver.save_to_file(user)
Here, the User
class is responsible only for storing user information, while UserDataSaver
handles file operations. This separation of concerns makes the code more maintainable.
2. Open/Closed Principle (OCP)
The Open/Closed Principle states that a class should be open for extension but closed for modification. This means that we should be able to add new functionality without modifying existing code.
In this example, we introduce a Discount
class and extend it using inheritance instead of modifying the original class.
class Discount:
def apply_discount(self, price):
return price
class SeasonalDiscount(Discount):
def apply_discount(self, price):
return price * 0.9 # 10% discount
class ClearanceDiscount(Discount):
def apply_discount(self, price):
return price * 0.7 # 30% discount
# Applying different discounts
original_price = 100
seasonal = SeasonalDiscount()
print("Seasonal Discount Price:", seasonal.apply_discount(original_price))
clearance = ClearanceDiscount()
print("Clearance Discount Price:", clearance.apply_discount(original_price))
Here, the base Discount
class is extended by SeasonalDiscount
and ClearanceDiscount
without modifying the original implementation.
Output:
Seasonal Discount Price: 90.0
Clearance Discount Price: 70.0
3. Liskov Substitution Principle (LSP)
The Liskov Substitution Principle states that a subclass should be able to replace its superclass without affecting the behavior of the program.
In this example, we create a base class Bird
and its subclasses. The subclass Ostrich
does not override fly()
because it cannot fly, ensuring correct behavior.
class Bird:
def fly(self):
return "I can fly!"
class Sparrow(Bird):
pass # Inherits fly behavior
class Ostrich(Bird):
def fly(self):
return "I cannot fly!"
# Using the classes
birds = [Sparrow(), Ostrich()]
for bird in birds:
print(bird.fly())
Here, replacing a Bird
instance with an Ostrich
does not break the behavior of the program.
Output:
I can fly!
I cannot fly!
4. Interface Segregation Principle (ISP)
The Interface Segregation Principle states that a class should not be forced to implement methods it does not need.
In this example, we use separate interfaces for printers that only print and those that also scan.
from abc import ABC, abstractmethod
class Printer(ABC):
@abstractmethod
def print_document(self):
pass
class Scanner(ABC):
@abstractmethod
def scan_document(self):
pass
class BasicPrinter(Printer):
def print_document(self):
return "Printing document..."
class MultiFunctionPrinter(Printer, Scanner):
def print_document(self):
return "Printing document..."
def scan_document(self):
return "Scanning document..."
# Using the classes
basic = BasicPrinter()
print(basic.print_document())
multi = MultiFunctionPrinter()
print(multi.print_document())
print(multi.scan_document())
Here, BasicPrinter
only implements print_document()
, while MultiFunctionPrinter
implements both print_document()
and scan_document()
.
Output:
Printing document...
Printing document...
Scanning document...
5. Dependency Inversion Principle (DIP)
The Dependency Inversion Principle states that high-level modules should depend on abstractions rather than concrete implementations.
class EmailService:
def send_email(self, message):
print("Sending email:", message)
class Notification:
def __init__(self, service):
self.service = service
def notify(self, message):
self.service.send_email(message)
# Using the classes
email_service = EmailService()
notification = Notification(email_service)
notification.notify("Hello, SOLID!")
Here, Notification
depends on an abstraction of an email service rather than a specific implementation.
Output:
Sending email: Hello, SOLID!