DEV Community

Timevolt
Timevolt

Posted on

The One Ring of SOLID: Keeping Your Code Open for Extension, Closed for Modification

The Quest Begins (The "Why")

I still remember the first time I tried to add a new shape to a geometry library I’d inherited. The file was a single ShapeCalculator class with a gargantuan area() method that looked like this:

def area(self, shape_type, **kwargs):
    if shape_type == "circle":
        return 3.14159 * kwargs["radius"] ** 2
    elif shape_type == "square":
        return kwargs["side"] ** 2
    elif shape_type == "rectangle":
        return kwargs["width"] * kwargs["height"]
    # …and on and on…
Enter fullscreen mode Exit fullscreen mode

Every time the product team asked for a triangle, a hexagon, or a weird custom polygon, I had to crack open that method, drop another elif branch, and pray I didn’t fat‑finger an indentation. The worst part? A tiny typo in one branch could silently break the area calculation for every other shape that had been working fine for months. I felt like I was trying to juggle flaming swords while blindfolded—exhausting, error‑prone, and frankly terrifying.

That’s when I realized I was fighting a dragon whose scales were made of if/else statements. I needed a better weapon, and that weapon turned out to be one of the SOLID principles: the Open/Closed Principle (OCP).

The Revelation (The Insight)

The Open/Closed Principle says, in plain English: “Software entities should be open for extension, but closed for modification.” Think of it like a LEGO baseplate. You can snap new bricks onto it to build ever‑cooler creations, but you never have to chisel away at the baseplate itself to make those bricks fit.

When I first grasped OCP, it felt like discovering a cheat code in a retro game. Instead of editing the core logic every time a new feature arrived, I could simply add new extensions—new classes that plug into the existing design without touching the battle‑tested code. The payoff? Fewer bugs, faster feature delivery, and a codebase that actually enjoys growing up.

Wielding the Power (Code & Examples)

The Trap: Violating OCP

Let’s look at the “before” code again, but this time with a tiny test to illustrate the pain:

class ShapeCalculator:
    def area(self, shape_type, **kwargs):
        if shape_type == "circle":
            return 3.14159 * kwargs["radius"] ** 2
        elif shape_type == "square":
            return kwargs["side"] ** 2
        elif shape_type == "rectangle":
            return kwargs["width"] * kwargs["height"]
        else:
            raise ValueError(f"Unknown shape: {shape_type}")

calc = ShapeCalculator()
print(calc.area("circle", radius=5))          # 78.53975
print(calc.area("triangle", base=4, height=3))# ValueError!
Enter fullscreen mode Exit fullscreen mode

Every new shape means editing area(). If a teammate accidentally changes the elif order or forgets to handle a case, the whole module can start returning nonsense. The class is closed for extension (you can’t add a shape without touching it) and open for modification (you’re constantly editing it). That’s the exact opposite of what we want.

The Victory: Embracing OCP

Now let’s refactor using abstraction. We’ll define a base Shape class with an abstract area() method, then create concrete subclasses for each shape.

from abc import ABC, abstractmethod
import math

class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        pass

class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self) -> float:
        return math.pi * self.radius ** 2

class Square(Shape):
    def __init__(self, side: float):
        self.side = side

    def area(self) -> float:
        return self.side ** 2

class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height

# Adding a new shape? Just subclass Shape—no existing code touched!
class Triangle(Shape):
    def __init__(self, base: float, height: float):
        self.base = base
        self.height = height

    def area(self) -> float:
        return 0.5 * self.base * self.height
Enter fullscreen mode Exit fullscreen mode

Now the client code looks like this:

shapes = [
    Circle(5),
    Square(4),
    Rectangle(3, 6),
    Triangle(4, 3)
]

for s in shapes:
    print(f"{s.__class__.__name__}: {s.area():.2f}")
Enter fullscreen mode Exit fullscreen mode

What changed?

  • The ShapeCalculator monster is gone.
  • Each shape owns its own area logic.
  • To support a new shape, you only add a new subclass—nothing else needs to change.
  • Existing shapes are guaranteed to keep working because we never edited them.

Why Getting It Wrong Hurts

If we ignore OCP, the codebase becomes a fragile house of cards. I once saw a team spend an entire sprint just fixing regressions caused by a single elif tweak in a payment‑processing module. Deployments were delayed, trust eroded, and the team’s morale dipped like a character losing health in a boss fight.

When you follow OCP, you give future‑you (and your teammates) the gift of predictability. Adding features feels like adding a new LEGO tower to an already sturdy castle—exciting, not terrifying.

Why This New Power Matters

Adopting the Open/Closed Principle does more than tidy up a single method; it reshapes how you think about software design.

  • Maintainability drops dramatically—bugs stay localized because you’re not constantly surgically editing core logic.
  • Onboarding becomes smoother—new hires can read the abstract base class and instantly grasp the contract, then safely add their own implementations.
  • Testing gets easier—each shape can be unit‑tested in isolation, and you never have to mock a sprawling conditional block.
  • Your code feels *alive*—it welcomes change instead of resisting it.

In short, OCP turns your codebase from a rigid monument into a living ecosystem that evolves with the product’s needs.

Your Turn: Forge Your Own Weapon

Here’s a quest for you: find a class in your current project that smells like a giant if/elif/elif chain (maybe a service that dispatches actions based on a string, a parser that branches on token types, or a report generator that switches on format). Extract an abstract base class, move each variant into its own subclass, and watch the fear of editing that file melt away.

When you’ve done it, drop a comment below with a before/after snippet—or just tell me how it felt to add a new feature without touching the old code. I’m genuinely excited to hear your stories!

Happy coding, and may your extensions always be open and your modifications forever closed. 🚀

Top comments (0)