Open/Closed: add rules without editing the calculator

Represent each discount as a rule. New rules can be added without growing a central switch.

Time O(rules)Space O(rules)Code C++, Python and C#
Watch it animatedStep through it, with the running line highlighted. Free with an account.

The idea

The calculator is closed to edits for each new discount, but the design is open to adding another IDiscountRule implementation. The stable caller depends on a small contract instead of a growing list of discount cases.

Code

class DiscountRule {
public:
    virtual double apply(double subtotal) const = 0;
    virtual ~DiscountRule() = default;
};
class TenPercentOff : public DiscountRule {
public:
    double apply(double subtotal) const override { return subtotal * 0.9; }
};
class FixedFiveOff : public DiscountRule {
public:
    double apply(double subtotal) const override { return max(0.0, subtotal - 5); }
};
class Checkout {
public:
    double total(double subtotal,
        const DiscountRule& rule) const { return rule.apply(subtotal); }
};

int main() {
    Checkout checkout;
    cout << checkout.total(100, TenPercentOff()) << "\n";
}
from typing import Protocol

class DiscountRule(Protocol):
    def apply(self, subtotal: float) -> float: ...

class TenPercentOff:
    def apply(self, subtotal: float) -> float: return subtotal * 0.9

class FixedFiveOff:
    def apply(self, subtotal: float) -> float: return max(0, subtotal - 5)

class Checkout:
    def total(self, subtotal: float,
              rule: DiscountRule) -> float: return rule.apply(subtotal)

checkout = Checkout()
print(checkout.total(100, TenPercentOff()))
interface IDiscountRule {
    decimal Apply(decimal subtotal);
}
class TenPercentOff : IDiscountRule {
    public decimal Apply(decimal subtotal) => subtotal * 0.9m;
}
class FixedFiveOff : IDiscountRule {
    public decimal Apply(decimal subtotal) => Math.Max(0, subtotal - 5m);
}
class Checkout {
    public decimal Total(decimal subtotal,
        IDiscountRule rule) => rule.Apply(subtotal);
}

var checkout = new Checkout();
Console.WriteLine(checkout.Total(100m, new TenPercentOff()));

Tips

In the .NET library

Strategy is one common way to support OCP: each discount is a strategy. OCP describes the design goal; Strategy is one possible structure that helps reach it.