ByteWise

Open to being extended, Closed to being modified

Section 8: SOLID Principles — Open/Closed (OCP)

OOP Principles Used: Abstraction, Polymorphism

You already met this informally back in Section 0 ("LLD exists to make change cheap") and saw it in action in the Polymorphism section (the process_order if/else chain vs. order.execute()). Now let's formalize it properly, on its own.

Start with an everyday picture: renovating vs. rebuilding

Imagine your house needs a new room. A good contractor builds an extension — new walls, connected to the existing structure, without touching your existing kitchen, bedrooms, or plumbing. A bad contractor knocks down a load-bearing wall to fit the new room in — now your whole existing structure is at risk, and you have to re-inspect everything that used to work.

Good software design works the same way: adding new functionality should feel like building an extension, not knocking down walls.

💡🗣️ Conversational
A class should be open to being extended (you can add new behavior) but closed to being modified (you shouldn't have to edit its existing, already-tested code to do so).

Technical: Software entities (classes, modules, functions) should be open for extension but closed for modification — new functionality should be added by writing new code, not by altering existing code that other parts of the system already depend on and trust.

Why bother — what breaks without it? (The exact if/else problem from Polymorphism, formalized)

Bad — violates OCP:

class TaxCalculator:
    def calculate_tax(self, order_type, order):
        if order_type == "domestic":
            return order.price * order.quantity * 0.10
        elif order_type == "foreign":
            return order.price * order.quantity * 0.20
        elif order_type == "crypto":
            return order.price * order.quantity * 0.30
        # every new order type = editing this method again
⚠️🗣️ What's Wrong Here

Every time a new tax category is introduced (say, "ETF" tax rules), you must reopen this method and add another elif. This method is never "finished" — it grows forever, and every edit risks breaking the domestic/foreign/crypto logic that already worked and was already tested.

Good — follows OCP, using Abstraction (Type 2) + Polymorphism together:

from abc import ABC, abstractmethod

class TaxStrategy(ABC):
    @abstractmethod
    def calculate(self, order):
        pass

class DomesticTax(TaxStrategy):
    def calculate(self, order):
        return order.price * order.quantity * 0.10

class ForeignTax(TaxStrategy):
    def calculate(self, order):
        return order.price * order.quantity * 0.20

class CryptoTax(TaxStrategy):
    def calculate(self, order):
        return order.price * order.quantity * 0.30

Using it:

def process_tax(order, tax_strategy):
    return tax_strategy.calculate(order)

process_tax(my_order, DomesticTax())
process_tax(my_order, ForeignTax())
ℹ️🗣️ Now Adding "ETF Tax"
class EtfTax(TaxStrategy):
    def calculate(self, order):
        return order.price * order.quantity * 0.05

Nothing else changes. DomesticTax, ForeignTax, CryptoTax, and process_tax() are never touched. You only ever add a new class — you never edit an existing, working one. That's OCP.

C++ version:

class TaxStrategy {
public:
    virtual double calculate(const Order& order) = 0;
    virtual ~TaxStrategy() {}
};

class DomesticTax : public TaxStrategy {
public:
    double calculate(const Order& order) override {
        return order.price * order.quantity * 0.10;
    }
};

class ForeignTax : public TaxStrategy {
public:
    double calculate(const Order& order) override {
        return order.price * order.quantity * 0.20;
    }
};

// Adding EtfTax later = new class only, nothing above is touched
class EtfTax : public TaxStrategy {
public:
    double calculate(const Order& order) override {
        return order.price * order.quantity * 0.05;
    }
};

Addendum: What Does @abstractmethod Actually Add? (Clarified via Dog/Cat)

Step 1 — Same shape as the very first if/else problem

class TaxCalculator:
    def calculate_tax(self, order_type, order):
        if order_type == "domestic":
            return order.price * order.quantity * 0.10
        elif order_type == "foreign":
            return order.price * order.quantity * 0.20

This is doing the exact same thing as the very first Polymorphism if/else example: checking a string, then picking a formula based on it.

def make_sound(animal_type):
    if animal_type == "dog":
        print("Woof")
    elif animal_type == "cat":
        print("Meow")

Same pattern: check a string → run different logic. The tax if/else and the animal if/else are the same shape of problem, just different words.

Step 2 — Map DomesticTax/ForeignTax directly onto Dog/Cat

Recall the Dog/Cat classes from Polymorphism:

class Dog:
    def make_sound(self):
        print("Woof")

class Cat:
    def make_sound(self):
        print("Meow")

Two separate classes, each with their own version of a method with the same name (make_sound).

The tax version is the identical pattern, just renamed:

class DomesticTax:
    def calculate(self, order):
        return order.price * order.quantity * 0.10

class ForeignTax:
    def calculate(self, order):
        return order.price * order.quantity * 0.20

Mapping:

Dog/CatTax
DogDomesticTax
CatForeignTax
make_sound()calculate()
Dog's make_sound returns "Woof"DomesticTax's calculate returns 10%
Cat's make_sound returns "Meow"ForeignTax's calculate returns 20%

DomesticTax and ForeignTax are just two classes, each with their own version of a calculate method — exactly like Dog and Cat each had their own version of make_sound.

Step 3 — What does @abstractmethod add, that Dog/Cat didn't have?

Notice: in Dog/Cat, there was no shared parent class. Dog and Cat were two totally separate, unrelated classes that just happened to both have a method called make_sound. Python let you put them in the same list and call .make_sound() on each — no ABC, no @abstractmethod, nothing extra needed.

So why does the tax version add TaxStrategy with @abstractmethod at all?

Answer: it doesn't add new behavior. It adds a safety promise.

class TaxStrategy(ABC):
    @abstractmethod
    def calculate(self, order):
        pass

This line says: "Any class that claims to be a TaxStrategy MUST have a calculate method — or Python will refuse to let you create it."

Without that promise (plain classes, like Dog/Cat):

class DomesticTax:
    def calculate(self, order):
        return order.price * order.quantity * 0.10

class BrokenTax:
    pass   # forgot to write calculate()! Python won't stop you.

b = BrokenTax()
b.calculate(order)   # crashes at runtime — AttributeError, only discovered when it's called

With the ABC + @abstractmethod promise:

class TaxStrategy(ABC):
    @abstractmethod
    def calculate(self, order):
        pass

class BrokenTax(TaxStrategy):
    pass   # forgot calculate() again

b = BrokenTax()   # ERROR IMMEDIATELY — Python refuses to even create the object
ℹ️🗣️ The Difference, in One Sentence

Without ABC/@abstractmethod, a missing method is a bug you discover later, possibly in production, when it's finally called. With it, Python catches the mistake the moment you try to create the broken class — much earlier, much safer.

So TaxStrategy is NOT doing the "different tax formulas" workDomesticTax, ForeignTax, CryptoTax do that part, exactly like Dog/Cat did. TaxStrategy is just a contract-checker sitting above them, making sure nobody forgets to write calculate().

How OCP directly depends on the OOP pillars you already know

Technical: OCP is not a separate mechanism — it's an outcome achieved by combining Abstraction (Type 2: a shared interface like TaxStrategy) with Polymorphism (the caller invokes .calculate() without checking which subclass it has). Without those two pillars already in place, OCP would be impossible to achieve — you'd be forced back into if/else chains.

💡🗣️ Conversational
This is why we built Abstraction and Polymorphism before SOLID — OCP is really just "use an interface + polymorphism instead of an if/else on type." You already know how to do this; SOLID just gives it a name and tells you when to reach for it.

An important caution — OCP doesn't mean "never edit any class, ever"

💡🗣️ Conversational

OCP applies to code that's already stable and depended upon by others — not to every single line you ever write. If Order itself gains a genuinely new field everyone needs (like order_id), you edit Order — that's normal evolution, not an OCP violation. OCP specifically targets the pattern of "growing if/else or switch statements based on type," which is the exact trap that breaks as a system scales.

⚠️🧠 Check Yourself Before Moving On

In your own words — how does TaxStrategy let you add EtfTax without touching any existing class, and which two OOP pillars make that possible?