The world of software development is full of dynamism. In the quest to maintain code quality and ease of maintenance, the SOLID design principles come in handy. They were established by Robert C. Martin also known as Uncle Bob, and they form the basis for efficient software architecture. The following blog will discuss each of the SOLID principles to help you master them.
Table of Contents
What Are SOLID Design principles?
SOLID principles refers to five design principles that can be applied in order to create more understandable, flexible and maintainable software designs. Let’s get into each principle:
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
Single Responsibility Principle (SRP)
Imagine a situation where one class handles multiple responsibilities in a large code base. Debugging becomes difficult and changes might inadvertently affect unrelated functionalities.
Single Responsibility Principle (SRP) states that a class should have only one reason to change, meaning it should focus on a single responsibility. This enhances the class’s clarity, testability, and maintainability.
class Employee:
def __init__(self, name, position):
self.name = name
self.position = position
def calculate_payroll(self):
# Logic for payroll calculation
def generate_report(self):
# Logic for generating report
In this example, the Employee class is responsible for multiple tasks: initializing employee details, calculating payroll, and generating reports. This makes the class harder to maintain because any change in payroll logic or report generation would require modifying the Employee class.
Solution:
class Employee:
def __init__(self, name, position):
self.name = name
self.position = position
class PayrollCalculator:
def calculate_payroll(self, employee):
# Logic for payroll calculation
class ReportGenerator:
def generate_report(self, employee):
# Logic for generating report
the responsibilities are divided among three classes. Employee only holds employee details, PayrollCalculator handles payroll calculations, and ReportGenerator handles report generation. This makes each class simpler and more focused, adhering to SRP.
Uncle Bob emphasizes that SRP significantly reduces code complexity. His book “Clean Code: A Handbook of Agile Software Craftsmanship” highlights SRP as essential for achieving modularity.
Open/Closed Principle (OCP)
Still, if you need to introduce new features into your software system, a careful modification of an existing code might result in unexpected errors. In this case OCP promotes design which is open for extension without changing the existing code.
According to OCP software components should be made so that they can change their behavior by adding additional code rather than modifying it. This means that one can add more functionalities just by introducing new codes instead of altering old ones.
class Notification:
def send(self, message, type):
if type == "email":
# Send email
elif type == "sms":
# Send SMS
The Notification class uses conditional statements to decide how to send a message. Adding a new type of notification (e.g., push notification) would require modifying this class, which violates OCP.
Solution:
class Notification:
def send(self, message):
pass
class EmailNotification(Notification):
def send(self, message):
# Send email
class SMSNotification(Notification):
def send(self, message):
# Send SMS
class NotificationService:
def send_notification(self, notification, message):
notification.send(message)
By using inheritance and polymorphism, we can extend the Notification class with new types (e.g., EmailNotification, SMSNotification) without modifying the existing Notification class. The NotificationService class can send any type of notification, adhering to OCP.
More flexible systems are achieved as a result of using OCP principle according to Bertrand Meyer who came up with this point. His book on “Object-Oriented Software Construction” is an elaborate treatise on OCP.
Liskov Substitution Principle (LSP)
Replacement of a subclass with different parent class might cause unpredictable behaviors and few bugs.
LSP states objects of a superclass should be replaceable with objects of any subclass without affecting program correctness as Barbara Liskov introduced it.
class Bird:
def fly(self):
pass
class Sparrow(Bird):
def fly(self):
# Sparrow flies
class Ostrich(Bird):
def fly(self):
raise NotImplementedError("Ostriches cannot fly")
The Ostrich class violates LSP because it throws an exception when the fly
method is called. This means you cannot use Ostrich objects in place of Bird objects without causing errors.
Solution:
class Bird:
def move(self):
pass
class Sparrow(Bird):
def move(self):
# Sparrow flies
class Ostrich(Bird):
def move(self):
# Ostrich runs
By changing the method to move
instead of fly, both Sparrow and Ostrich can implement it in a way that makes sense for them. Sparrow flies and Ostrich runs. This ensures that Ostrich can be used in place of Bird without causing errors, adhering to LSP.
This principle guarantees the potentiality of subclasses to substitute their super classes.
LSP is one way of ensuring proper inheritance practices. In other words, subclasses should always abide by the agreements laid down by their parents.
Interface Segregation Principle (ISP)
Implementing and working with large monolithic interfaces can be heavy going. Take an interface containing methods that are not needed by a class but must be implemented all the same.
According to ISP, no client ought to use methods which it does not employ because this makes its language unreadable. Rather, interfaces ought to be small and specific in focus.
class MultiFunctionDevice:
def print(self, document):
pass
def scan(self, document):
pass
def fax(self, document):
pass
class OldPrinter(MultiFunctionDevice):
def print(self, document):
# Print document
def scan(self, document):
raise NotImplementedError("This printer cannot scan")
def fax(self, document):
raise NotImplementedError("This printer cannot fax")
The OldPrinter class is forced to implement methods it does not support, like scan and fax, leading to methods that throw exceptions. This violates ISP because the OldPrinter class depends on methods it doesn’t need.
Solution:
class Printer:
def print(self, document):
pass
class Scanner:
def scan(self, document):
pass
class Fax:
def fax(self, document):
pass
class OldPrinter(Printer):
def print(self, document):
# Print document
By breaking down the MultiFunctionDevice interface into smaller, more specific interfaces (Printer, Scanner, Fax), the OldPrinter class only needs to implement the print method, adhering to ISP.
ISP leads to less coupled code as argued by Robert C. Martin in his book “Agile Software Development,” Principles Patterns and Practices.” The book provides detailed information on ISP.
Dependency Inversion Principle (DIP)
When there is tight coupling between high-level modules and low-level ones, it becomes difficult for organizations to make changes on such systems thereby making them get stuck easily.
High-level modules should never depend on low-level modules; rather both should depend on abstractions, according to DIP. Moreover, abstractions do not depend upon details whereas details depend on abstractions.
class LightBulb:
def turn_on(self):
# Logic to turn on the light bulb
class Switch:
def __init__(self, bulb):
self.bulb = bulb
def operate(self):
self.bulb.turn_on()
The Switch class directly depends on the LightBulb class, making it hard to switch out the LightBulb with another device without modifying the Switch class. This violates DIP.
Solution:
class Switchable:
def turn_on(self):
pass
class LightBulb(Switchable):
def turn_on(self):
# Logic to turn on the light bulb
class Fan(Switchable):
def turn_on(self):
# Logic to turn on the fan
class Switch:
def __init__(self, device):
self.device = device
def operate(self):
self.device.turn_on()
# Usage
bulb = LightBulb()
switch = Switch(bulb)
switch.operate()
fan = Fan()
switch = Switch(fan)
switch.operate()
By introducing the Switchable interface, both LightBulb and Fan can implement this interface. The Switch class now depends on the Switchable abstraction, allowing it to work with any device that implements Switchable, adhering to DIP.
This principle is vital for achieving a decoupled architecture, making it easier to swap out implementations without affecting the system. Uncle Bob’s “Clean Architecture: A Craftsman’s Guide to Software Structure and Design” offers practical advice on applying DIP.
Conclusion
Mastering the SOLID design principles is essential for any developer aiming to create robust, maintainable, and scalable software. By adhering to SRP, OCP, LSP, ISP, and DIP, you’ll be well on your way to writing cleaner and more efficient code.
Sharing this knowledge helps improve not only our own skills but also contributes to a more professional and competent developer community. Remember, the key to great software design is continuous learning and application of best practices.
Feel free to share your thoughts or ask questions in the comments below. Happy coding!
References
- Robert C. Martin, “Clean Code: A Handbook of Agile Software Craftsmanship“
- Bertrand Meyer, “Object-Oriented Software Construction“
- Robert C. Martin, “Agile Software Development, Principles, Patterns, and Practices“
- Robert C. Martin, “Clean Architecture: A Craftsman’s Guide to Software Structure and Design“
You May Also Like