Hey there, fellow code enthusiasts! Whether you’re sipping rooibos tea in Cape Town or coding away in a Johannesburg office, if you’re building software, you’ll want to make sure it doesn’t fall over like a poorly constructed braai stand. Today, we’re diving into the secret sauce that keeps software strong, flexible, and not a complete headache to maintain: the SOLID principles!
What’s the Deal with SOLID?
Back in the day, a bloke named Robert C. Martin (aka “Uncle Bob”) came up with these principles as a recipe for crafting software that doesn’t make you want to toss your laptop into the nearest dam when you need to make changes six months down the line.
SOLID isn’t just another fancy tech acronym – it’s your ticket to software that can handle the twists and turns of changing requirements without throwing a wobbly. Think of it as building a sturdy rondavel rather than a flimsy shack that collapses in the first Highveld thunderstorm.
Let’s unpack what these letters stand for, hey?
- Single Responsibility Principle: Each class should do one thing only, boet.
- Open/Closed Principle: Your code should be open for extension but closed for modification.
- Liskov Substitution Principle: Derived classes must be substitutable for their base classes.
- Interface Segregation Principle: Don’t force clients to depend on methods they don’t use.
- Dependency Inversion Principle: Depend on abstractions, not on concrete implementations.
The Single Responsibility Principle: Stick to Your Lane!
Imagine you’re at a braai and someone’s trying to be the fire-starter, meat-flipper, salad-maker, AND DJ all at once. Chaos, right? That’s exactly what happens when a class has too many responsibilities.
The Single Responsibility Principle (SRP) says a class should have only one reason to change. In other words, each class should have a single job or responsibility.
Here’s a quick example in Python:
# Before: User class doing too much
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def save_user(self):
# Logic to save user to the database
print(f"User {self.name} saved to database.")
def send_welcome_email(self):
# Logic to send welcome email
print(f"Welcome email sent to {self.email}")
That’s like asking your goalkeeper to also be your striker – not a lekker plan!
Instead, let’s split the responsibilities:
# After: Each class has one responsibility
class User:
def __init__(self, name, email):
self.name = name
self.email = email
class UserRepository:
def save(self, user):
# Logic to save user to the database
print(f"User {user.name} saved to database.")
class EmailService:
def send_welcome_email(self, user):
# Logic to send welcome email
print(f"Welcome email sent to {user.email}")
Now we’re cooking with gas! Each class does exactly one thing, making your code more organized than a perfectly packed Checkers shopping bag.
The Open/Closed Principle: No Need to Break What’s Working!
The Open/Closed Principle (OCP) is like having a bakkie that you can add accessories to without disassembling the engine. Your software should be open for extension (adding new features) but closed for modification (not changing existing, working code).
Let’s check out an example:
# Before: Area calculator that needs modification for each new shape
class AreaCalculator:
def calculate_area(self, shape):
if isinstance(shape, Rectangle):
return shape.width * shape.height
elif isinstance(shape, Circle):
return 3.14 * shape.radius ** 2
return 0
Every time we add a new shape, we’d need to change this method. Not ideal!
Here’s a better approach:
# After: Using polymorphism to extend without modifying
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 ** 2
class AreaCalculator:
def calculate_area(self, shape):
return shape.area()
Now we can add as many shapes as we want without touching the AreaCalculator. Howzit!
The Liskov Substitution Principle: Keep Your Promises!
The Liskov Substitution Principle (LSP) is like promising your mates you’ll bring a 4×4 for a Kruger Park trip, but showing up with something that can actually handle the terrain – not a city slicker’s car that will get stuck in the first bit of sand.
In programming terms, if B is a subclass of A, then objects of type A can be replaced with objects of type B without affecting the functionality of the program.
Here’s the classic square-rectangle problem:
# Violation: A square is not substitutable for a rectangle
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def set_width(self, width):
self.width = width
def set_height(self, height):
self.height = height
def area(self):
return self.width * self.height
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
def set_width(self, width):
super().set_width(width)
super().set_height(width) # Breaking Rectangle's behavior
def set_height(self, height):
super().set_height(height)
super().set_width(height) # Breaking Rectangle's behavior
This code will cause confusion faster than a taxi changing lanes without indicators! Better solution:
# Solution: Different shape classes implementing a common interface
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 Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
Now both shapes fulfill the same interface correctly. Proper job!
The Interface Segregation Principle: Small is Beautiful!
The Interface Segregation Principle (ISP) is like a restaurant menu – you don’t want to wade through pages of food options if you’re just looking for a koeksister with your coffee.
ISP says clients shouldn’t be forced to depend on interfaces they don’t use. Rather create smaller, more specific interfaces than one big, fat interface.
Let’s check out an example:
# Before: One large interface forces implementers to provide methods they might not need
class Worker:
def work(self):
pass
def eat(self):
pass
class HumanWorker(Worker):
def work(self):
print("Human working.")
def eat(self):
print("Human eating.")
class RobotWorker(Worker):
def work(self):
print("Robot working.")
def eat(self):
# Robots don't eat, but forced to implement this method
pass
This is like forcing everyone at a braai to play rugby – not everyone’s cup of tea!
Let’s fix it:
# After: Segregated interfaces allow classes to implement only what they need
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 working.")
def eat(self):
print("Human eating.")
class RobotWorker(Workable):
def work(self):
print("Robot working.")
# No need to implement eat() method!
Now that’s sorted – each class only implements what makes sense for it!
The Dependency Inversion Principle: Abstractions for the Win!
The Dependency Inversion Principle (DIP) is like telling your GPS your destination instead of memorizing every turn – you’re relying on an abstraction (the GPS), not the concrete details (every street name).
DIP says high-level modules shouldn’t depend on low-level modules – both should depend on abstractions. And abstractions shouldn’t depend on details – details should depend on abstractions.
Here’s what we want to avoid:
# Before: Direct dependency on concrete implementation
class ConcreteUserRepository:
def get_user_by_id(self, user_id):
# Logic to retrieve user from a specific database
return {"name": "John Doe", "email": "john.doe@example.com"}
class UserService:
def __init__(self):
self.user_repository = ConcreteUserRepository() # Hard dependency!
def get_user(self, user_id):
return self.user_repository.get_user_by_id(user_id)
This is as flexible as trying to do the Diski dance in gumboots!
Here’s a better way:
# After: Depending on abstractions
from abc import ABC, abstractmethod
class UserRepository(ABC):
@abstractmethod
def get_user_by_id(self, user_id):
pass
class ConcreteUserRepository(UserRepository):
def get_user_by_id(self, user_id):
# Logic to retrieve user from a specific database
return {"name": "John Doe", "email": "john.doe@example.com"}
class UserService:
def __init__(self, user_repository: UserRepository):
self.user_repository = user_repository # Dependency injection!
def get_user(self, user_id):
return self.user_repository.get_user_by_id(user_id)
Now we can swap out different repositories without changing the service – as easy as switching between Castle and Black Label at a braai!
Keeping It Real: SOLID in the Wild
Look, we’ve all been there – deadlines looming, the product owner making wild feature requests, and the temptation to just slap some code together and call it a day. But that’s how you end up with a codebase that’s shakier than a Springbok supporter’s nerves during a World Cup final!
Applying SOLID principles isn’t always straightforward, and yes, you can over-engineer things if you’re not careful. The trick is finding that sweet spot – like getting the perfect amount of peri-peri sauce on your chicken: enough to give it flavor without burning your mouth off!
Start small:
- Identify the smelliest part of your codebase (we all have them!)
- Apply one principle at a time
- Use code reviews to keep everyone on track
- Remember that perfect is the enemy of good
Why Bother with All This?
You might be thinking, “Is all this effort really worth it?” The answer is ja, definitely! Here’s why:
- Your future self will thank you when you can add new features without breaking everything
- New team members won’t run away screaming when they see your code
- Testing becomes much easier when components are properly separated
- Your application becomes more resilient to change
- You’ll spend less time putting out fires and more time building cool new features
Now, is it as SOLID as a Rock?
SOLID principles aren’t just fancy theory – they’re practical tools that can help you build software that’s as robust as Table Mountain and as adaptable as a South African who’s lived through load shedding.
So, next time you’re tempted to chuck everything into one massive class or hardcode dependencies all over the shop, remember these principles. Your code will be better for it, your team will be happier, and you’ll sleep better knowing your software isn’t held together with the coding equivalent of sticky tape and hope.
Now go forth and code SOLIDly, my friends! And remember, in the wise words we often hear around these parts: “A lekker codebase is a happy codebase!”
About the author: A software developer who’s survived enough spaghetti code to feed the whole of Ellis Park Stadium, and is on a mission to make code as organized as a perfectly packed Tetris board.






Leave a Reply