Phase 5 · Low-level design: OOP and SOLID · Topic 5.4

SOLID: O — Open/Closed Principle

Must know

Code should be open for extension but closed for modification: add new behaviour by adding code, not by editing code that already works.

Not started

Step 1 Understand it

The problem it solves. Every time a new type of thing appears (a new payment method, a new discount), developers edit the same big if/else, and each edit risks breaking cases that already worked.

New words in this topic (tap one to see what it means)

Watch it happen

AnimationOpen/closed: add behaviour by adding classes, not editing old code
Read the animation as text
  1. A price calculator that switches on the discount type with if/elif. Every new kind of discount means editing this method.
  2. Adding a student discount means opening working code and adding another branch — risking the cases that already work.
  3. Instead, depend on an interface: DiscountRule with apply(price). Each branch becomes its own small class; the calculator just asks every rule it is given.
  4. A new discount is a new class. The calculator never changes: open for extension, closed for modification.

Picture it

classDiagram
  class DiscountRule {
    <<interface>>
    +apply(price)
  }
  DiscountRule <|.. SaleDiscount
  DiscountRule <|.. StudentDiscount
  DiscountRule <|.. AnyNewRule
  PriceCalculator --> DiscountRule : uses a list of
The calculator only knows the DiscountRule . A new discount is a new plugged in at the bottom; the calculator never changes.

The idea

A long if/elif chain on type ('if card … elif paypal …') must be edited every time a new case appears, which risks breaking the old cases. Instead, depend on an interface and add one new class for each new case.

How it works, step by step

  1. Find the code that switches on a type: if kind == 'sale' … elif kind == 'student' …
  2. Define an interface for the behaviour that varies: apply(price).
  3. Move each branch into its own class that implements the interface.
  4. The main code just calls rule.apply(price) for every rule it's given.
  5. A new discount is a new class. The main code never changes.

Analogy

A power strip: you add a new device by plugging it in, without rewiring the house.

Where the analogy breaks

A power strip has a fixed number of sockets, designed in advance. You can't predict every future change, so apply this where variation is likely, not everywhere.

Example

A price calculator takes a list of discount rules. Adding a 'student discount' means adding one new rule class, with no change to the calculator.

Must-know points

  • Replace 'switch on type' with .
  • , and plug-in designs all follow this principle.
  • Protects tested code from new changes.

Common mistakes

  • Adding for variations that will never happen.
  • Still having to edit one central if/else to create the new class. Use a registry or configuration instead.
python
class DiscountRule:
    def apply(self, price): return price

class SaleDiscount(DiscountRule):
    def apply(self, price): return price * 0.9

class StudentDiscount(DiscountRule):        # a new rule: no edits elsewhere
    def apply(self, price): return price - 5

def final_price(price, rules):
    for rule in rules:
        price = rule.apply(price)
    return price

Step 2 Go further (free, optional)

Optional. The notes above are written to be enough on their own. These free resources give a second explanation and extra practice.

Step 3 Practise

Try each exercise yourself first: on paper, out loud or in code. Then open the answer and compare it with yours.

  1. ExplainSay it in plain words in under a minute, with no notes.

    Explain the open/closed principle with the power-strip picture.

    Show answer

    With a power strip, you add a new device by plugging it in. You don't rewire the house, and the kettle and the lamp that are already plugged in aren't affected.

    The open/closed principle says code should work the same way: open for extension (you can add new behaviour), but closed for modification (you don't edit code that already works). The strip's sockets are the interface: every device has the same plug.

    In code: a price calculator that loops over "discount rules", each with an apply() method. A new student discount is a new rule class plugged into the list. Compare that with a long if/elif on the discount type, which you must edit, and might break, every time a new kind appears.

  2. DrawSketch the diagram (or class diagram) from memory.

    Draw the DiscountRule design as a .

    Show answer

    final_price depends only on the DiscountRule interface. Each concrete rule implements apply() its own way. A new rule is a new box at the bottom; nothing above it changes.

    text
    final_price(price, rules) ── uses ──► DiscountRule
                                          + apply(price)
                                               △
                         ┌─────────────────────┼─────────────────────┐
                    SaleDiscount        StudentDiscount         (new rules
                    apply: price × 0.9  apply: price − £5        plug in here)
  3. ApplyUse it in a real design question and say exactly where it fits.

    Rewrite a shipping-cost function that uses if/elif for 'standard', 'express' and 'international', so that adding 'drone' needs no edits to existing code.

    Show answer

    Before, a single function had an if/elif branch per method, so adding "drone" meant editing it (and retesting every branch). After, each method is its own class behind a Shipping interface, and the function just looks the method up in a registry. Adding drone delivery is one new class and one line that registers it; shipping_cost and the existing are untouched.

    python
    from abc import ABC, abstractmethod
    
    
    class Shipping(ABC):
        @abstractmethod
        def cost(self, weight_kg): ...
    
    
    class Standard(Shipping):
        def cost(self, weight_kg):
            return 3.0 + 0.5 * weight_kg
    
    
    class Express(Shipping):
        def cost(self, weight_kg):
            return 8.0 + 1.0 * weight_kg
    
    
    class International(Shipping):
        def cost(self, weight_kg):
            return 15.0 + 2.5 * weight_kg
    
    
    METHODS = {"standard": Standard(), "express": Express(), "international": International()}
    
    
    def shipping_cost(method, weight_kg):       # never edited again
        return METHODS[method].cost(weight_kg)
    
    
    print(shipping_cost("express", 2))           # 10.0
    
    
    class Drone(Shipping):                       # the new method: one class...
        def cost(self, weight_kg):
            if weight_kg > 2:
                raise ValueError("too heavy for a drone")
            return 12.0
    
    
    METHODS["drone"] = Drone()                   # ...and one line to register it
    print(shipping_cost("drone", 1.5))           # 12.0
  4. Trade-offsSay what it costs, when not to use it, and the alternative.

    Where would applying this principle be over-engineering?

    Show answer

    • Two cases that will probably never become three: VAT on or off, a guest or a member. One if is clearer than an interface and two classes.
    • Code you own and can easily change: a private function in one service. If a third case appears, refactor then; editing your own code isn't a crime.
    • When you don't yet know how it will vary. Plug-in points built on guesses usually end up in the wrong place, and make the real change harder (YAGNI: you aren't gonna need it).
    • When it is really data, not behaviour: shipping prices that differ only in numbers belong in a table or config, not in one class per row.
    • Scripts and prototypes that will be thrown away.

    Apply it where change is frequent or expected (pricing rules, payment methods, plug-ins), or where other teams or customers extend your code without being able to edit it.

Step 4 Interview-ready, then prove it

Your interview answer

About 20 to 40 seconds, said out loud. Practise it until it sounds like you, not like notes.

Open/closed means I can add behaviour without editing existing code — for example, new discount types as new classes behind a DiscountRule interface, instead of a growing if/else chain.

Follow-up questions

Does it mean you never edit code?

No. You still fix bugs and refactor. It means designing so that the variations you expect don't require changing working code.

Tick each check when you can do it without notes

A topic is finished when you pass all four checks, not when you finish a video. Be strict: if you needed your notes, it is not ticked yet.

  • Explain Say it in plain words in under a minute, with no notes.
  • Draw Sketch the diagram (or class diagram) from memory.
  • Apply Use it in a real design question and say exactly where it fits.
  • Trade-offs Say what it costs, when not to use it, and the alternative.

0 of 4 checks ticked.

Saved in this browser. Back it up or move it to another device.