DEV Community

Cover image for SOLID Principles in Software Development
Vivek Yadav
Vivek Yadav

Posted on

SOLID Principles in Software Development

In the realm of software development, the SOLID principles are a set of five design principles aimed at creating robust, maintainable, and scalable software systems. These principles, coined by Robert C. Martin (also known as Uncle Bob), provide a guideline for developers to follow to ensure their codebase is clean and extensible. Here, we will explore each of the SOLID principles and demonstrate how to implement them with examples in Python.

1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change, meaning it should have only one job or responsibility.

Example:

class Order:
    def __init__(self, items):
        self.items = items

    def calculate_total(self):
        return sum(item.price for item in self.items)

class InvoicePrinter:
    @staticmethod
    def print_invoice(order):
        print("Invoice:")
        for item in order.items:
            print(f"{item.name}: ${item.price}")
        print(f"Total: ${order.calculate_total()}")

# Usage
class Item:
    def __init__(self, name, price):
        self.name = name
        self.price = price

items = [Item("Apple", 1), Item("Banana", 2)]
order = Order(items)
InvoicePrinter.print_invoice(order)
Enter fullscreen mode Exit fullscreen mode

In this example, the Order class is responsible only for managing the order, while the InvoicePrinter class is responsible for printing the invoice. This adheres to SRP by ensuring each class has a single responsibility.

2. Open/Closed Principle (OCP)

Definition: Software entities should be open for extension but closed for modification.

Example:

class Discount:
    def apply(self, total):
        return total

class PercentageDiscount(Discount):
    def __init__(self, percentage):
        self.percentage = percentage

    def apply(self, total):
        return total - (total * self.percentage / 100)

class FixedDiscount(Discount):
    def __init__(self, amount):
        self.amount = amount

    def apply(self, total):
        return total - self.amount

def calculate_total(order, discount):
    total = order.calculate_total()
    return discount.apply(total)

# Usage
discount = PercentageDiscount(10)
print(calculate_total(order, discount))
Enter fullscreen mode Exit fullscreen mode

In this example, the Discount class is extended by PercentageDiscount and FixedDiscount without modifying the base class, adhering to OCP.

3. Liskov Substitution Principle (LSP)

Definition: Subtypes must be substitutable for their base types without altering the correctness of the program.

Example:

class Bird:
    def fly(self):
        pass

class Sparrow(Bird):
    def fly(self):
        print("Sparrow is flying")

class Ostrich(Bird):
    def fly(self):
        raise Exception("Ostrich can't fly")

def make_bird_fly(bird):
    bird.fly()

# Usage
sparrow = Sparrow()
make_bird_fly(sparrow)

ostrich = Ostrich()
try:
    make_bird_fly(ostrich)
except Exception as e:
    print(e)
Enter fullscreen mode Exit fullscreen mode

Here, Ostrich violates LSP because it cannot fly, thus it is not substitutable for the Bird base class.

4. Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on interfaces they do not use.

Example:

from abc import ABC, abstractmethod

class Printer(ABC):
    @abstractmethod
    def print_document(self, document):
        pass

class Scanner(ABC):
    @abstractmethod
    def scan_document(self, document):
        pass

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

    def scan_document(self, document):
        print(f"Scanning: {document}")

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

# Usage
mfp = MultiFunctionPrinter()
mfp.print_document("Report")
mfp.scan_document("Report")

printer = SimplePrinter()
printer.print_document("Report")
Enter fullscreen mode Exit fullscreen mode

In this example, the MultiFunctionPrinter implements both Printer and Scanner interfaces, while SimplePrinter only implements Printer, adhering to ISP.

5. Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

Example:

from abc import ABC, abstractmethod

class Database(ABC):
    @abstractmethod
    def save(self, data):
        pass

class MySQLDatabase(Database):
    def save(self, data):
        print("Saving data to MySQL database")

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

class UserService:
    def __init__(self, database: Database):
        self.database = database

    def save_user(self, user_data):
        self.database.save(user_data)

# Usage
mysql_db = MySQLDatabase()
mongo_db = MongoDBDatabase()

user_service = UserService(mysql_db)
user_service.save_user({"name": "John Doe"})

user_service = UserService(mongo_db)
user_service.save_user({"name": "Jane Doe"})
Enter fullscreen mode Exit fullscreen mode

In this example, the UserService depends on the Database abstraction, allowing for flexibility and adhering to DIP.

Conclusion

By adhering to the SOLID principles, developers can create software that is more modular, easier to maintain, and scalable. These principles help in managing the complexities of software development, ensuring that code remains clean and extensible. Through practical examples in Python, we can see how these principles can be applied to create robust and maintainable systems.

Top comments (0)