SOLID Principles: Building Software That Stands the Test of Time

SOLID Principles: Building Software That Stands the Test of Time
The short URL of the present article is: https://buzzcube.co.za/go/i7yq

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:

  1. Identify the smelliest part of your codebase (we all have them!)
  2. Apply one principle at a time
  3. Use code reviews to keep everyone on track
  4. 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.

🤞 Get Notified On New Posts!

We don’t spam! Read our privacy policy for more info.

Get Notified On New Posts!

We don’t spam! Read our privacy policy for more info.

The short URL of the present article is: https://buzzcube.co.za/go/i7yq
Richard Soderblom

Leave a Reply

Your email address will not be published. Required fields are marked *

Oh hi there 👋
It’s nice to meet you.

Sign up to receive awesome content in your inbox, every month.

We don’t spam! Read our privacy policy for more info.