SOLID Principles (Image Credit: FreeCodeCamp)
SOLID is an acronym that stands for five design principles that help developers create more maintainable, understandable, and flexible software. Let's go through each one with a relatable example.
1. S - Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change, meaning it should have only one job or responsibility.
Explanation: Imagine you have a tool that combines two different tasks, like sending emails and processing payments. If both tasks are handled by a single class, changes in the email feature might break the payment feature. By keeping these responsibilities separate, you minimise the risk of changes in one part affecting another.
Example:
class EmailSender:
def send_email(self, recipient, subject, body):
# Code to send an email
print(f"Sending email to {recipient} with subject '{subject}'")
class PaymentProcessor:
def process_payment(self, amount):
# Code to process payment
print(f"Processing payment of amount {amount}")
# Usage
email_sender = EmailSender()
email_sender.send_email("user@example.com", "Hello!", "Welcome to our service!")
payment_processor = PaymentProcessor()
payment_processor.process_payment(100)
In this example, EmailSender
is responsible only for sending emails, and PaymentProcessor
is responsible only for processing payments. They each have a single responsibility, making the code easier to maintain and extend.
2. O - Open/Closed Principle (OCP)
Definition: Software entities (like classes, modules, functions, etc.) should be open for extension but closed for modification.
Explanation: This means you should be able to add new features or behaviors to a class without changing its existing code. Imagine you have a payment processing system, and you want to add a new payment method. You should be able to add this new method without modifying existing code.
Example:
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount):
pass
class CreditCardPayment(PaymentProcessor):
def process_payment(self, amount):
print(f"Processing credit card payment of {amount}")
class PayPalPayment(PaymentProcessor):
def process_payment(self, amount):
print(f"Processing PayPal payment of {amount}")
# Usage
payments = [CreditCardPayment(), PayPalPayment()]
for payment in payments:
payment.process_payment(100)
In this example, PaymentProcessor
is an abstract class that defines a contract for processing payments. CreditCardPayment
and PayPalPayment
are implementations that extend this class. If you want to add a new payment method, you can create a new class that extends PaymentProcessor
without modifying existing classes.
3. L - Liskov Substitution Principle (LSP)
Definition: Subtypes must be substitutable for their base types without altering the correctness of the program.
Explanation: This means that objects of a superclass should be replaceable with objects of a subclass without affecting the functionality of the program. For example, if you have a function that works with a Vehicle
class, it should also work with any subclass like Car
or Bike
.
Example:
class Vehicle:
def start_engine(self):
pass
class Car(Vehicle):
def start_engine(self):
print("Starting car engine...")
class Bike(Vehicle):
def start_engine(self):
print("Starting bike engine...")
# Usage
def start_vehicle_engine(vehicle: Vehicle):
vehicle.start_engine()
car = Car()
bike = Bike()
start_vehicle_engine(car) # Should work fine
start_vehicle_engine(bike) # Should work fine
In this example, Car
and Bike
are subclasses of Vehicle
. The start_vehicle_engine
function can work with any subclass of Vehicle
without needing to know the specifics of the subclass, which is in line with the Liskov Substitution Principle.
4. I - Interface Segregation Principle (ISP)
Definition: A client should not be forced to implement interfaces it does not use. Instead of one fat interface, many small interfaces are preferred based on groups of methods, each one serving one submodule.
Explanation: This principle suggests that you should create specific interfaces for each type of client rather than one general-purpose interface. Imagine you have a machine that can print, scan, and fax. If you have separate machines that can only print or scan, they shouldn't be forced to implement functionalities they don't use.
Example:
from abc import ABC, abstractmethod
class Printer(ABC):
@abstractmethod
def print(self, document):
pass
class Scanner(ABC):
@abstractmethod
def scan(self, document):
pass
class MultiFunctionDevice(Printer, Scanner):
def print(self, document):
print(f"Printing: {document}")
def scan(self, document):
print(f"Scanning: {document}")
# Usage
mfd = MultiFunctionDevice()
mfd.print("Document 1")
mfd.scan("Document 2")
Here, Printer
and Scanner
are separate interfaces. MultiFunctionDevice
implements both, but if there were devices that only printed or only scanned, they wouldn't need to implement methods they don't use, adhering to the Interface Segregation Principle.
5. D - Dependency Inversion Principle (DIP)
Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces). Abstractions should not depend on details. Details should depend on abstractions.
Explanation: Instead of a high-level class depending directly on low-level classes, both should depend on an interface or an abstract class. This allows for more flexibility and easier maintenance.
Example:
from abc import ABC, abstractmethod
class NotificationService(ABC):
@abstractmethod
def send(self, message):
pass
class EmailNotificationService(NotificationService):
def send(self, message):
print(f"Sending email: {message}")
class SMSNotificationService(NotificationService):
def send(self, message):
print(f"Sending SMS: {message}")
class NotificationSender:
def __init__(self, service: NotificationService):
self.service = service
def notify(self, message):
self.service.send(message)
# Usage
email_service = EmailNotificationService()
sms_service = SMSNotificationService()
notifier = NotificationSender(email_service)
notifier.notify("Hello via Email")
notifier = NotificationSender(sms_service)
notifier.notify("Hello via SMS")
In this example, NotificationSender
depends on the NotificationService
abstraction rather than on a concrete class like EmailNotificationService
or SMSNotificationService
. This way, you can switch the notification service without changing the NotificationSender
class.
Conclusion
Single Responsibility Principle (SRP): A class should do one thing and do it well.
Open/Closed Principle (OCP): A class should be open for extension but closed for modification.
Liskov Substitution Principle (LSP): Subclasses should be substitutable for their base classes.
Interface Segregation Principle (ISP): No client should be forced to depend on methods it does not use.
Dependency Inversion Principle (DIP): Depend on abstractions, not on concrete implementations.
By following these SOLID principles, you can create software that is easier to understand, maintain, and extend.
Top comments (0)