Building Blocks of Better Code: An Introduction to SOLID Principles

Building Blocks of Better Code: An Introduction to SOLID Principles

By Sylvester Das

August 28, 2025

6 min read

Have you ever felt like your codebase is a tangled mess, ready to collapse at any moment? Do you dread making changes because you know it will create a cascade of unexpected bugs? If so, you're not alone! Many developers struggle with maintaining clean, scalable, and understandable code. That's where the SOLID principles come in.

SOLID is a set of five design principles that, when followed, help you create software that is easier to maintain, extend, and test. Think of them as the foundational building blocks for robust and reliable applications. They might seem a bit abstract at first, but trust me, understanding and applying them will significantly improve your coding skills and save you countless headaches down the road.

This article will introduce you to each of the SOLID principles with clear explanations and practical examples using Python. We'll break down each principle, explore common pitfalls, and demonstrate how to apply them effectively.

The SOLID Acronym: A Quick Overview

SOLID is an acronym, with each letter representing a different principle:

  • S - Single Responsibility Principle (SRP)

  • O - Open/Closed Principle (OCP)

  • L - Liskov Substitution Principle (LSP)

  • I - Interface Segregation Principle (ISP)

  • D - Dependency Inversion Principle (DIP)

Let's dive into each principle one by one.

1. Single Responsibility Principle (SRP)

Concept: A class should have only one reason to change. In other words, a class should have only one job.

Why it Matters: When a class has multiple responsibilities, changes to one responsibility can unintentionally affect others, leading to unexpected bugs and making the class harder to understand and maintain.

Bad Example:

class User:
    def __init__(self, username, email):
        self.username = username
        self.email = email

    def save_to_database(self):
        # Code to save user data to the database
        print(f"Saving user {self.username} to the database.")

    def send_welcome_email(self):
        # Code to send a welcome email to the user
        print(f"Sending welcome email to {self.email}.")

In this example, the User class is responsible for both managing user data and handling database interactions and email sending. This violates the SRP.

Good Example:

class User:
    def __init__(self, username, email):
        self.username = username
        self.email = email

class UserRepository:
    def save(self, user):
        # Code to save user data to the database
        print(f"Saving user {user.username} to the database.")

class EmailService:
    def send_welcome_email(self, user):
        # Code to send a welcome email to the user
        print(f"Sending welcome email to {user.email}.")

#Usage
user = User("johndoe", "john.doe@example.com")
user_repo = UserRepository()
email_service = EmailService()

user_repo.save(user)
email_service.send_welcome_email(user)

Now, each class has a single responsibility: User manages user data, UserRepository handles database interactions, and EmailService handles email sending.

2. Open/Closed Principle (OCP)

Concept: Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

Why it Matters: You should be able to add new functionality to a class without modifying its existing code. This reduces the risk of introducing bugs and simplifies maintenance.

Bad Example:

class Shape:
    def __init__(self, type, width=0, height=0):
        self.type = type
        self.width = width
        self.height = height

def area(shape):
    if shape.type == "rectangle":
        return shape.width * shape.height
    elif shape.type == "circle":
        return 3.14 * (shape.width/2) * (shape.width/2) #Assuming width is diameter
    #Imagine adding more shapes here...

rectangle = Shape("rectangle", 5, 10)
circle = Shape("circle", 4)
print(f"Rectangle area: {area(rectangle)}")
print(f"Circle area: {area(circle)}")

Adding a new shape requires modifying the area function, violating the OCP.

Good 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_area(shape):
    return shape.area()

rectangle = Rectangle(5, 10)
circle = Circle(2)
print(f"Rectangle area: {calculate_area(rectangle)}")
print(f"Circle area: {calculate_area(circle)}")

Now, adding a new shape only requires creating a new class that inherits from Shape and implements the area method. The calculate_area function remains unchanged.

3. Liskov Substitution Principle (LSP)

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

Why it Matters: If a subclass cannot be used in place of its parent class without causing errors, it violates the LSP and introduces unexpected behavior.

Bad Example:

class Bird:
    def fly(self):
        print("Flying!")

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

def make_bird_fly(bird):
    bird.fly()

bird = Bird()
ostrich = Ostrich()

make_bird_fly(bird)  # Works fine
try:
    make_bird_fly(ostrich) # Raises an error
except Exception as e:
    print(e)

The Ostrich class cannot be used in place of the Bird class because it throws an error when fly() is called.

Good Example:

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def move(self):
        pass

class Bird(Animal):
    def move(self):
        print("Flying!")

class Ostrich(Animal):
    def move(self):
        print("Running!")

def make_animal_move(animal):
    animal.move()

bird = Bird()
ostrich = Ostrich()

make_animal_move(bird)
make_animal_move(ostrich)

Instead of inheriting fly from bird, both Bird and Ostrich inherit the more general move from Animal. Now, both classes can be used interchangeably without unexpected errors.

4. Interface Segregation Principle (ISP)

Concept: Clients should not be forced to depend on methods they do not use.

Why it Matters: Large interfaces can lead to classes implementing methods they don't need, making the code bloated and harder to maintain.

Bad Example:

class Worker:
    def work(self):
        pass

    def eat(self):
        pass

class HumanWorker(Worker):
    def work(self):
        print("Human is working")

    def eat(self):
        print("Human is eating")

class RobotWorker(Worker):
    def work(self):
        print("Robot is working")

    def eat(self):
        # Robots don't eat, but we have to implement this method
        pass

The RobotWorker class is forced to implement the eat method, even though it doesn't need it.

Good Example:

from abc import ABC, abstractmethod

class Workable(ABC):
    @abstractmethod
    def work(self):
        pass

class Eatable(ABC):
    @abstractmethod
    def eat(self):
        pass

class HumanWorker(Workable, Eatable):
    def work(self):
        print("Human is working")

    def eat(self):
        print("Human is eating")

class RobotWorker(Workable):
    def work(self):
        print("Robot is working")

Now, the interfaces are segregated. HumanWorker implements both Workable and Eatable, while RobotWorker only implements Workable.

5. Dependency Inversion Principle (DIP)

Concept: 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.

Why it Matters: This principle promotes loose coupling between modules, making the code more flexible and testable.

Bad Example:

class LightBulb:
    def turn_on(self):
        print("LightBulb: turned on...")

    def turn_off(self):
        print("LightBulb: turned off...")

class Switch:
    def __init__(self):
        self.bulb = LightBulb()

    def operate(self):
        self.bulb.turn_on()

The Switch class directly depends on the LightBulb class, creating tight coupling.

Good Example:

from abc import ABC, abstractmethod

class Switchable(ABC):
    @abstractmethod
    def turn_on(self):
        pass

    @abstractmethod
    def turn_off(self):
        pass

class LightBulb(Switchable):
    def turn_on(self):
        print("LightBulb: turned on...")

    def turn_off(self):
        print("LightBulb: turned off...")

class Switch:
    def __init__(self, device: Switchable):
        self.device = device

    def operate(self):
        self.device.turn_on()

bulb = LightBulb()
switch = Switch(bulb)
switch.operate()

Now, both Switch and LightBulb depend on the Switchable abstraction, decoupling them and making the code more flexible.

Practical Implications

Applying the SOLID principles might seem like extra work initially, but the long-term benefits are significant. By following these principles, you can:

  • Reduce code complexity: Easier to understand and maintain.

  • Increase code reusability: Components can be easily reused in different parts of the application.

  • Improve testability: Easier to write unit tests for individual components.

  • Enhance code flexibility: Easier to adapt to changing requirements.

  • Minimize bugs: Reduce the risk of introducing new bugs when making changes.

Conclusion

The SOLID principles are essential for writing maintainable, scalable, and robust software. While they might seem abstract at first, understanding and applying them will significantly improve your coding skills and save you time and effort in the long run. Start incorporating these principles into your projects today, and you'll see a noticeable difference in the quality of your code.


Share this article

Advertisement

Shorten Your Links, Amplify Your Reach

Tired of long, clunky URLs? Create short, powerful, and trackable links with MiniFyn. It's fast, free, and easy to use.


Follow Us for Updates