Sign In
Sign In

Understanding SOLID: The First 5 Principles of Object-Oriented Design

Understanding SOLID: The First 5 Principles of Object-Oriented Design
Adnene Mabrouk
Technical writer
18.07.2024
Reading time: 6 min

In the realm of software development, adhering to certain design principles can significantly impact the quality, maintainability, and scalability of your codebase. One such set of principles that has stood the test of time is SOLID—a mnemonic acronym for five principles of object-oriented design. These principles were introduced by Robert C. Martin in the early 2000s and have since become a cornerstone for building robust and flexible software systems.

Single Responsibility Principle (SRP)

The Single Responsibility Principle states that a class should have only one reason to change, meaning it should have only one job or responsibility within the software system. By focusing each class on a single concern, SRP 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: Printing invoice
        print(f"Invoice amount: {self.amount}")

class InvoicePersistence:
    def save_to_file(self, invoice):
        # Responsibility: Saving invoice to a file
        with open('invoice.txt', 'w') as f:
            f.write(f"Invoice amount: {invoice.amount}")

# Separate responsibilities into different classes
invoice = Invoice(100)
invoice.print_invoice()

persistence = InvoicePersistence()
persistence.save_to_file(invoice)

Open/Closed Principle (OCP)

The Open/Closed Principle emphasizes that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This principle encourages developers to design systems in a way that allows new functionality to be added through extension rather than by changing existing code, thereby minimizing the risk of introducing bugs in already working code.

Example:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

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

def calculate_total_area(shapes):
    total = 0
    for shape in shapes:
        total += shape.area()
    return total

shapes = [Rectangle(10, 20), Circle(5)]
print(calculate_total_area(shapes))

Liskov Substitution Principle (LSP)

The Liskov Substitution Principle defines that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. In other words, derived classes must be substitutable for their base classes without altering the desirable properties of the program, 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("Ostrich can't fly")

def make_bird_fly(bird: Bird):
    bird.fly()

sparrow = Sparrow()
make_bird_fly(sparrow)

ostrich = Ostrich()
# make_bird_fly(ostrich)  # This will raise an exception, violating LSP

Interface Segregation Principle (ISP)

The Interface Segregation Principle advocates for designing fine-grained, client-specific interfaces rather than one large interface that caters to multiple clients. By segregating interfaces based on client requirements, ISP prevents clients from depending on interfaces they do not use, thereby minimizing the impact of changes and promoting 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 MultiFunctionPrinter(Printer, Scanner):
    def print_document(self, document):
        print(f"Printing: {document}")

    def scan_document(self):
        print("Scanning document")

class SimplePrinter(Printer):
    def print_document(self, document):
        print(f"Printing: {document}")

# Clients use only the interfaces they need
mfp = MultiFunctionPrinter()
mfp.print_document("Report")
mfp.scan_document()

printer = SimplePrinter()
printer.print_document("Report")

Dependency Inversion Principle (DIP)

The Dependency Inversion Principle suggests that high-level modules should not depend on low-level modules but rather both should depend on abstractions. This principle promotes decoupling and reduces the dependency between classes by introducing interfaces or abstract classes that define the contract between them, facilitating easier 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} to MySQL database")

class MongoDBDatabase(Database):
    def save(self, data):
        print(f"Saving {data} to MongoDB database")

class DataHandler:
    def __init__(self, db: Database):
        self.db = db

    def save_data(self, data):
        self.db.save(data)

# High-level module depends on abstraction (interface)
mysql_db = MySQLDatabase()
handler = DataHandler(mysql_db)
handler.save_data("Some data")

mongodb = MongoDBDatabase()
handler = DataHandler(mongodb)
handler.save_data("Some other data")

Applying SOLID Principles in Practice

While understanding the SOLID principles is crucial, their true value is realized when applied in practice. By incorporating these principles into your software design process, you can achieve code that is more modular, easier to test, and less prone to bugs. Practicing SRP ensures each class has a clear and focused responsibility, OCP encourages designing for change without modifying existing code, LSP fosters consistency and substitutability, ISP enhances interface clarity and client-specificity, and DIP promotes flexibility and reduces coupling.

Conclusion

SOLID principles provide a structured approach to designing object-oriented systems that are not only easier to maintain and extend but also more resilient to change and scalable over time. By internalizing these principles and applying them judiciously, developers can elevate the quality of their software designs and deliver more robust solutions that meet evolving business requirements. By understanding and implementing SOLID principles, developers can craft software systems that are more maintainable, adaptable, and easier to scale, ensuring longevity and efficiency in software development projects.

18.07.2024
Reading time: 6 min

Do you have questions,
comments, or concerns?

Our professionals are available to assist you at any moment,
whether you need help or are just unsure of where to start.
Email us
Hostman's Support