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