DEV Community

Robin 2077
Robin 2077

Posted on

SOLID Principles - Explained Using Real World Examples in Python

SOLID Principles

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)