Swipe left or right to navigate to next or previous post
SOLID principles are a set of five design principles intended to make software designs more understandable, flexible, and maintainable. They were introduced by Robert C. Martin, also known as Uncle Bob, in the early 2000s. They were introduced in his 2000 paper “Design Principles and Design Patterns” to help developers write software that is easy to understand, modify, and extend.
The SOLID principles offer developers a framework for organizing their code, resulting in software that is both flexible and easy to modify and test. By following these principles, developers can create code that is modular, maintainable, and scalable, fostering effective collaboration within development teams. They exist as a universal law between developers which they should apply to all their work.
The SOLID principles are as follows:
The Single Responsibility Principle (SRP) emphasizes that a class should have only one reason to change. In other words, it should have only one responsibility or concern. This principle encourages developers to design classes that are focused and cohesive, with each class responsible for a specific piece of functionality. By adhering to SRP, classes become more maintainable, as changes to one aspect of the system are less likely to affect other unrelated aspects.
Suppose we have a class called EmailService that is responsible for sending emails. Initially, this class might handle both the composition of the email message and the actual sending of the email:
class EmailComposer:
@staticmethod
def compose_email(recipient, subject, body):
message = f"To: {recipient}\n"
message += f"Subject: {subject}\n\n"
message += body
return message
class EmailSender:
@staticmethod
def send_email(message):
# Code to send the email
# (Assume some implementation details here)
print("Sending email:\n", message)
class EmailService:
def __init__(self):
self.email_composer = EmailComposer()
self.email_sender = EmailSender()
def send_email(self, recipient, subject, body):
message = self.email_composer.compose_email(recipient, subject, body)
self.email_sender.send_email(message)
# Example usage
email_service = EmailService()
email_service.send_email("[email protected]", "Hello", "This is a test email.")
In this example:
Each class has a single responsibility: composing emails, sending emails, and orchestrating the process, respectively. This adheres to the Single Responsibility Principle, making the code easier to maintain and understand.
The Open/Closed Principle (OCP) advocates for software entities to be open for extension but closed for modification. This means that once a module, class, or function is written and tested, it should not be modified to add new functionality. Instead, new functionality should be added by extending the existing code through inheritance, composition, or other mechanisms. By following OCP, developers can create systems that are resilient to changes and easier to maintain.
Suppose we have a class hierarchy representing different shapes, and we want to calculate the area of each shape. We'll demonstrate how to design this system following the OCP:
from abc import ABC, abstractmethod
# Abstract base class for shapes
class Shape(ABC):
@abstractmethod
def area(self):
pass
# Concrete implementations of shapes
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
# A class responsible for calculating the total area of shapes
class AreaCalculator:
@staticmethod
def calculate_total_area(shapes):
total_area = 0
for shape in shapes:
total_area += shape.area()
return total_area
# Example usage
shapes = [Rectangle(5, 4), Circle(3)]
total_area = AreaCalculator.calculate_total_area(shapes)
print("Total area:", total_area)
In this example:
By adhering to the OCP, we can easily add new shapes to our system without altering existing code in the AreaCalculator class. This design promotes code reuse, maintainability, and scalability.
The Liskov Substitution Principle (LSP) states that objects of a superclass should be substitutable with objects of its subclasses without altering the correctness of the program. In other words, a subclass should behave in such a way that it can be used interchangeably with its superclass without causing unexpected behavior. This principle ensures that inheritance hierarchies are well-designed and that subclasses adhere to the contracts defined by their superclasses, promoting polymorphism and code reuse.
Suppose we have a class hierarchy representing different types of birds. We'll demonstrate how to design this system following LSP:
class Bird:
def fly(self):
pass
class Sparrow(Bird):
def fly(self):
print("Sparrow flying")
class Ostrich(Bird):
def fly(self):
# Ostriches cannot fly, so this method should not be implemented for them
raise NotImplementedError("Ostrich cannot fly")
# Function to perform flying action for any bird
def perform_fly(bird):
bird.fly()
# Example usage
sparrow = Sparrow()
ostrich = Ostrich()
perform_fly(sparrow) # Output: Sparrow flying
perform_fly(ostrich) # Raises NotImplementedError: Ostrich cannot fly
In this example:
The Interface Segregation Principle (ISP) suggests that clients should not be forced to depend on interfaces they do not use. Instead of one large interface, it's better to have multiple smaller interfaces that are specific to the needs of the clients. By adhering to ISP, developers can prevent classes from having unnecessary dependencies, making the system more modular and easier to maintain. This principle helps in avoiding "fat" interfaces that contain methods irrelevant to certain clients.
Suppose we have a system that manages electronic devices such as printers and scanners. We'll design the system using ISP:
from abc import ABC, abstractmethod
# Define separate interfaces for printer and scanner functionalities
class Printer(ABC):
@abstractmethod
def print_document(self, document):
pass
class Scanner(ABC):
@abstractmethod
def scan_document(self):
pass
# Concrete implementations of printer and scanner
class LaserPrinter(Printer):
def print_document(self, document):
print("Printing document using laser printer:", document)
class InkjetPrinter(Printer):
def print_document(self, document):
print("Printing document using inkjet printer:", document)
class FlatbedScanner(Scanner):
def scan_document(self):
print("Scanning document using flatbed scanner")
class SheetfedScanner(Scanner):
def scan_document(self):
print("Scanning document using sheetfed scanner")
# A class representing a multifunction printer that implements both printer and scanner interfaces
class MultifunctionPrinter(Printer, Scanner):
def print_document(self, document):
print("Printing document using multifunction printer:", document)
def scan_document(self):
print("Scanning document using multifunction printer")
# Example usage
laser_printer = LaserPrinter()
laser_printer.print_document("Example document")
flatbed_scanner = FlatbedScanner()
flatbed_scanner.scan_document()
In this example:
The Dependency Inversion Principle (DIP) advocates for high-level modules to depend on abstractions rather than concrete implementations. This principle states that both high-level and low-level modules should depend on abstractions, and abstractions should not depend on details. By adhering to DIP, developers can create systems that are loosely coupled, making it easier to replace or modify components without affecting other parts of the system. Dependency injection and inversion of control are common techniques used to implement DIP.
Suppose we have a high-level module NotificationService that sends notifications to users via email and SMS. We'll design the system using DIP:
from abc import ABC, abstractmethod
# Define an abstract interface for notification delivery
class NotificationProvider(ABC):
@abstractmethod
def send_notification(self, message):
pass
# Concrete implementations for email and SMS notification providers
class EmailNotificationProvider(NotificationProvider):
def send_notification(self, message):
print("Sending email notification:", message)
class SMSNotificationProvider(NotificationProvider):
def send_notification(self, message):
print("Sending SMS notification:", message)
# High-level module responsible for sending notifications using the NotificationProvider interface
class NotificationService:
def __init__(self, notification_provider):
self.notification_provider = notification_provider
def send_notification(self, message):
self.notification_provider.send_notification(message)
# Example usage
email_provider = EmailNotificationProvider()
notification_service = NotificationService(email_provider)
notification_service.send_notification("Hello, this is an email notification")
sms_provider = SMSNotificationProvider()
notification_service = NotificationService(sms_provider)
notification_service.send_notification("Hello, this is an SMS notification")
In this example: