S-O-L-I-D is a set of object-oriented design principles that seeks to improve software quality and flexibility. The acronym SOLID stands for five fundamental principles that help developers create more robust, maintainable and scalable software systems. These principles are:
S - Single Responsibility Principle: Each class should have a single responsibility or reason for change.
O - Open/Closed Principle: Software should be open for extension, but closed for modification.
L - Liskov Substitution Principle: Objects of a derived class must be substitutable for objects of the base class without altering the operation of the program.
I - Interface Segregation Principle: Clients must not be forced to depend on interfaces that they do not use.
D - Dependency Inversion Principle: Dependencies should be in abstraction, not in concrete details.
In this blog, you will learn about each principle of the S-O-L-I-D approach to understand its fundamentals and how to apply them in object-oriented design.
The Single Responsibility Principle states that a class should have only one reason to change, meaning it should have only one task or responsibility within the software system. By focusing each class on a single concern, the Single Responsibility Principle reduces complexity, improves readability, and makes code easier to maintain and extend.
Example:
class Invoice:
def __init__(self, amount):
self.amount = amount
def print_invoice(self):
# Responsibility: Print invoice
print(f"Invoice amount: {self.amount}")
class InvoicePersistence:
def save_to_file(self, invoice):
# Responsibility: Save invoice to file
with open('invoice.txt', 'w') as f:
f.write(f"Invoice amount: {invoice.amount}")
# Sepate responsibilities into different classes
invoice = Invoice(100)
invoice.print_invoice()
persistence = InvoicePersistence()
persistence.save_to_file(invoice)
The Open/Closed Principle emphasizes that software entities (classes, modules, functions, etc.) should be open to extension but closed to modification. This principle encourages developers to design systems in a way that allows new functionality to be added through extension rather than changing existing code, thus minimizing the risk of introducing bugs into code that already works.
Example:
from abc import ABC, abstractmethod
class Figure(ABC):
@abstractmethod
def area(self):
pass
class Rectangle(Figure):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Figure):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
def calculate_total_area(figures):
total = 0
for figure in figures:
total += figure.area()
return total
figures = [Rectangle(10, 20), Circle(5)]
print(calculate_total_area(figures))
The Liskov Substitution Principle defines that objects of a superclass must be substitutable for objects of its subclasses without affecting the correctness of the program. In other words, derived classes must be able to substitute their base classes without altering the desirable properties of the program, thus ensuring consistency in behavior and design.
Example:
class Bird:
def fly(self):
print("Flying")
class sparrow(Bird):
def fly(self):
print("Sparrow flying")
class ostrich(Bird):
def fly(self):
raise Exception("Ostriches cannot fly")
def make_fly(bird: Bird):
bird.fly()
sparrow = sparrow()
make_fly(sparrow)
ostrich = ostrich()
# make_fly(ostrich) # This will throw an exception, violating LSP
The Interface Segregation Principle advocates designing customer-specific, fine-grained interfaces rather than a single large interface serving multiple customers. By segregating interfaces based on customer requirements, the ISP prevents customers from relying on interfaces they do not use, which minimizes the impact of changes and promotes better code organization.
Example:
from abc import ABC, abstractmethod
class Printer(ABC):
@abstractmethod
def print_document(self, document):
pass
class Scanner(ABC):
@abstractmethod
def scan_document(self):
pass
class AllInOnePrinter(Printer, Scanner):
def print_document(self, document):
print(f"Printing: {document}")
def scan_document(self):
print("Scanning document")
class ImpresoraSimple(Printer):
def print_document(self, document):
print(f"Printing: {document}")
# Clients use only the interfaces they need
allinone_printer = AllInOnePrinter()
allinone_printer.print_document("Report")
allinone_printer.scan_document()
simple_printer = ImpresoraSimple()
simple_printer.print_document("Report")
The Dependency Inversion Principle suggests that high-level modules should not depend on low-level modules, but that both should depend on abstractions. This principle promotes decoupling and reduces dependency between classes by introducing interfaces or abstract classes that define the contract between them, facilitating maintenance, testing and scalability.
Example:
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def save(self, data):
pass
class MySQLDatabase(Database):
def save(self, data):
print(f"Saving {data} in MySQL Database")
class MongoDatabase(Database):
def save(self, data):
print(f"Save {data} in Mongo Database")
class DataManagement:
def __init__(self, db: Database):
self.db = db
def save_data(self, data):
self.db.save(data)
# Module height level depends on abstraction (interface)
mysql_db = MySQLDatabase()
manager = DataManagement(mysql_db)
manager.save_data("Some data")
mongodb = MongoDatabase()
manager = DataManagement(mongodb)
manager.save_data("Other data")
While understanding the S-O-L-I-D principles is essential, their true value manifests itself when they are implemented in practice. By integrating these principles into software design, more modular, easier to test, and less error-prone code can be achieved. The application of SRP ensures that each class has a specific and well-defined responsibility, OCP promotes design that adapts to changes without the need to alter existing code, LSP promotes consistency and interchangeability of classes, ISP improves clarity of interfaces and adaptability to customer requirements, and DIP promotes flexibility and reduces coupling between components.
The S-O-L-I-D principles provide structured guidance for designing object-oriented systems that are not only easier to maintain and extend, but also more resilient to change and scalable over the long term. By properly internalizing and applying these principles, developers can significantly improve the quality of their software designs and provide more robust solutions that respond to changing business requirements. Understanding and implementing the S-O-L-I-D principles enables developers to build software systems that are more maintainable, adaptable and scalable, thus ensuring durability and efficiency in software development projects.