System design can feel like building a complex structure. Without a strong foundation, your system can become brittle, difficult to maintain, and prone to collapse under pressure. That's where the SOLID principles come in. These five principles, when applied correctly, act as the sturdy foundation for building robust, scalable, and maintainable software systems.
Let's break them down in the easiest way possible:
Think of building a house. You want it to be strong, easy to fix, and easy to add to later. That’s what SOLID principles do for software.
1. One Thing Per Room-Single Responsibility Principle (SRP)
What it means:
Each room in your house has one main job. The kitchen is for cooking, the bedroom is for sleeping, etc.
In code:
Each “part” of your software should do one thing well. Don’t try to make your kitchen also be the garage.
Why it's good:
If the kitchen sink breaks, you don't have to rebuild the whole house. You just fix the kitchen.
# House Analogy: Kitchen does cooking, bedroom does sleeping.
# Code Example:
class Kitchen:
def cook_meal(self, meal):
print(f"Cooking {meal} in the kitchen.")
class Bedroom:
def sleep(self):
print("Sleeping in the bedroom.")
# Bad Example (mixing responsibilities):
# class MultiPurposeRoom:
# def cook_meal(self, meal):
# # ...
# def sleep(self):
# # ...
my_kitchen = Kitchen()
my_bedroom = Bedroom()
my_kitchen.cook_meal("dinner")
my_bedroom.sleep()
2. Adding Rooms Without Tearing Down Walls (Open/Closed Principle)
What it means:
You should be able to add a new room (like a sunroom) without having to knock down existing walls.
In code:
You should be able to add new features to your software without changing the old code.
Why it's good:
You don't accidentally break the living room while building the sunroom.
class HouseRoom:
def area(self):
pass # Base room, can't calculate area alone
class Bedroom(HouseRoom):
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
class Sunroom(HouseRoom):
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
def calculate_room_area(room):
print(f"Room area: {room.area()}")
my_bedroom = Bedroom(10, 12)
my_sunroom = Sunroom(8, 8)
calculate_room_area(my_bedroom)
calculate_room_area(my_sunroom)
# Bad Example (modifying existing code to add new room types):
# def calculate_room_area(room, room_type):
# if room_type == "bedroom":
# print(f"Bedroom area: {room.length * room.width}")
# elif room_type == "sunroom":
# print(f"Sunroom area: {room.length * room.width}")
# # ... (would need to modify this function for every new room type)
3.Using Interchangeable Parts (Liskov Substitution Principle)
What it means:
Think of standard sized building materials. If you design a door frame for a "standard door," you should be able to put in any door that fits that standard size, whether it's a wooden door, a metal door, or a glass door. All these doors should "fit" where a "standard door" is expected
In code:
If you have a general "shape" for something (like a "door"), all the specific types of that thing (like "wooden door," "metal door") should work in the same way wherever you use the general "shape."
Why it's good:
It means you can easily swap out parts without having to rebuild the whole structure. If you need a more secure door, you can just replace the wooden one with a metal one, and it should still fit the same frame.
class Door:
def fit(self, frame_width, frame_height):
pass
class WoodenDoor(Door):
def fit(self, frame_width, frame_height):
print(f"Wooden door fitting in {frame_width}x{frame_height} frame.")
class MetalDoor(Door):
def fit(self, frame_width, frame_height):
print(f"Metal door fitting in {frame_width}x{frame_height} frame.")
def install_door(door, frame_width, frame_height):
door.fit(frame_width, frame_height)
my_wooden_door = WoodenDoor()
my_metal_door = MetalDoor()
install_door(my_wooden_door, 36, 80)
install_door(my_metal_door, 36, 80)
# Bad Example (violating LSP):
# class Wall:
# def fit(self, frame_width, frame_height):
# raise Exception("Walls do not fit in door frames.") # Wall doesnt work where door is expected.
#
# my_wall = Wall()
# install_door(my_wall, 36, 80) # This would fail.
4. Only the Tools You Need (Interface Segregation)
What it means:
If you’re building a house, you don’t carry around every tool. You bring the hammer for nails, the saw for wood.
In code:
If a part of your software only needs a few abilities, don’t give it all the abilities. Give it just the ones it needs.
Why it's good:
It keeps things simple. The hammer doesn’t get in the way when you’re using the saw.
class HammerTool:
def hammer_nails(self):
print("Hammering nails.")
class SawTool:
def cut_wood(self):
print("Cutting wood.")
class SimpleBuilder:
def use_hammer(self, hammer):
hammer.hammer_nails()
class Carpenter:
def use_hammer(self, hammer):
hammer.hammer_nails()
def use_saw(self, saw):
saw.cut_wood()
my_hammer = HammerTool()
my_saw = SawTool()
builder = SimpleBuilder()
builder.use_hammer(my_hammer)
carpenter = Carpenter()
carpenter.use_hammer(my_hammer)
carpenter.use_saw(my_saw)
# Bad Example (large interface, not all builders need all tools):
# class BuilderTool:
# def hammer_nails(self):
# pass
# def cut_wood(self):
# pass
#
# class SimpleBuilder:
# def use_tool(self, tool: BuilderTool):
# tool.hammer_nails()
#
# class Carpenter:
# def use_tool(self, tool: BuilderTool):
# tool.hammer_nails()
# tool.cut_wood()
5. Don’t Care Who Made the Pipes, Just That They Work (Dependency Inversion)
What it means:
You don’t care who made the pipes in your house, just that they carry water.
In code:
Your software shouldn’t depend on specific “parts.” It should depend on general “ways of doing things.”
Why it's good:
If the pipe company goes out of business, you can easily switch to another brand of pipes. The water still flows.
class WaterSource:
def supply_water(self):
pass
class Pipe:
def __init__(self, source: WaterSource):
self.source = source
def deliver_water(self):
self.source.supply_water()
print("Water delivered through pipe.")
class CityWater(WaterSource):
def supply_water(self):
print("City water supply.")
class WellWater(WaterSource):
def supply_water(self):
print("Well water supply.")
city_water = CityWater()
well_water = WellWater()
my_pipe = Pipe(city_water)
my_pipe.deliver_water()
my_pipe = Pipe(well_water)
my_pipe.deliver_water()
# Bad Example (depending on concrete implementations):
# class Pipe:
# def __init__(self, city_water): # Hard dependency on CityWater
# self.city_water = city_water
#
# def deliver_water(self):
# self.city_water.supply_water()
# print("Water delivered through pipe.")
#
# my_pipe = Pipe(CityWater()) # Would break if we needed to use WellWater.
In Simple Words:
SOLID helps you build software that:
- Is easy to understand.
- Is easy to change.
- Is less likely to break when you change it.
- Is easy to reuse parts of.
- It's like building a house that’s well-organized, easy to fix, and easy to add to later. You would never build a house where changing the kitchen broke the bathroom, and neither should you build software that way.
Top comments (0)