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.
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.
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.
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.
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.
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.
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.
The above is the detailed content of SOLID Principles - Explained Using Real World Examples in Python. For more information, please follow other related articles on the PHP Chinese website!