Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Low-Level Design (Object-Oriented Design) Module 4 – Design principles: SOLID and beyond

Open/Closed Principle (OCP)

The Open/Closed Principle in practice: add a vehicle type without editing tested code, choose closed sets or interfaces, and know when a seam is premature.

  • Intermediate
  • 25 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7 and Node.js 24.21.0
  • By MySmartCoPilot

What you will learn

  • Explain "open for extension, closed for modification" with a worked example
  • Add a new variant through a seam instead of editing switch statements
  • Choose between an interface and a closed set with exhaustive matching
  • Decide when a new seam is worth adding and when it is premature

Before you start

On this page

Most new requirements are new variants of something the program already does: another vehicle type, another discount, another payment method. The Open/Closed Principle (OCP) asks for a design where a new variant is new code that you add, not old code that you edit. This lesson builds that on a parking lot, measures what it saves, and then looks at the cases where it is the wrong tool.

Open for extension, closed for modification

Bertrand Meyer stated the principle in his book Object-Oriented Software Construction, first published in 1988: modules should be both open and closed. Open means you can still extend a module, for instance by giving it more operations or more fields. Closed means other modules can already depend on it: its interface is settled, and it could be compiled and shipped as a library. The two sound contradictory, and with the techniques of the time they were. Meyer’s answer was inheritance: a closed class stays as it is, and a new class extends it.

Robert C. Martin restated it for classes: it should be possible to add to what a class does without editing the class. His means are abstractions. The code that holds the policy depends on an interface, and each variant is a new implementation of it. He calls plugin systems, programs that other people extend without touching their code, the strongest form of the idea.

In daily work, “closed” means that the code that already works, and its tests, stay untouched when a variant is added. “Open” means there is a place where the new variant plugs in. Such a place is called a seam in this track.

A switch that every new kind must edit

Here is a small parking program in the most direct style. Every function that depends on the kind of vehicle tests for each kind:

Fees, spots and labels chosen by if statements

fees/switch/v1/main.py

from receipt import receipt

for vehicle, hours in [("bike", 3), ("car", 2), ("truck", 1)]:
    print(receipt(vehicle, hours))

Output

Two-wheeler  small    3 h    30
Car          medium   2 h    50
Truck        large    1 h    60

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 main.py

fees/switch/v1/fees.py

def fee(vehicle, hours):
    """What a stay of `hours` whole hours (at least 1) costs, in whole currency units."""
    if vehicle == "bike":
        return 10 * hours
    if vehicle == "car":
        return 30 + 20 * (hours - 1)
    if vehicle == "truck":
        return 60 * hours
    raise ValueError(f"unknown vehicle type: {vehicle}")

fees/switch/v1/spots.py

def spot_size(vehicle):
    """Which size of parking spot a vehicle needs."""
    if vehicle == "bike":
        return "small"
    if vehicle == "car":
        return "medium"
    if vehicle == "truck":
        return "large"
    raise ValueError(f"unknown vehicle type: {vehicle}")

fees/switch/v1/receipt.py

from fees import fee
from spots import spot_size

LABELS = {"bike": "Two-wheeler", "car": "Car", "truck": "Truck"}


def receipt(vehicle, hours):
    return f"{LABELS[vehicle]:<12} {spot_size(vehicle):<8} {hours} h  {fee(vehicle, hours):>4}"

Nothing is wrong with this code today. The trouble starts when the lot adds electric cars: fees.py, spots.py and receipt.py each hold their own list of vehicle kinds, and each list must grow. Meyer described the same trap and named the rule that avoids it, the Single Choice principle:

Whenever a software system must support a set of alternatives, one and only one module in the system should know their exhaustive list.

A seam: a registry of vehicle types

The second design gives every kind of vehicle a class with the three things the lot needs, a label, a spot size and a fee(hours) method. vehicles.py describes that shape as a typing.Protocol and keeps a registry, and receipt.py asks the registry for a kind and never names one:

Each kind of vehicle is a class; the registry holds them

fees/plugins/v1/main.py

import standard
from receipt import receipt
from vehicles import Registry

registry = Registry()
standard.install(registry)

for vehicle, hours in [("bike", 3), ("car", 2), ("truck", 1)]:
    print(receipt(registry, vehicle, hours))

Output

Two-wheeler  small    3 h    30
Car          medium   2 h    50
Truck        large    1 h    60

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 main.py

fees/plugins/v1/vehicles.py

"""What the parking lot needs from any kind of vehicle, and the registry that holds the kinds.

Closed for modification: adding a kind of vehicle never edits this file.
"""
from typing import Protocol


class VehicleType(Protocol):
    label: str
    spot: str

    def fee(self, hours: int) -> int:
        """What a stay of `hours` whole hours (at least 1) costs, in whole currency units."""
        ...


class Registry:
    def __init__(self):
        self._types: dict[str, VehicleType] = {}

    def add(self, name: str, vehicle_type: VehicleType) -> None:
        if name in self._types:
            raise ValueError(f"{name!r} is registered already")
        self._types[name] = vehicle_type

    def get(self, name: str) -> VehicleType:
        if name not in self._types:
            raise ValueError(f"unknown vehicle type: {name}")
        return self._types[name]

fees/plugins/v1/standard.py

"""The kinds of vehicle the lot opened with."""


class Bike:
    label, spot = "Two-wheeler", "small"

    def fee(self, hours):
        return 10 * hours


class Car:
    label, spot = "Car", "medium"

    def fee(self, hours):
        return 30 + 20 * (hours - 1)


class Truck:
    label, spot = "Truck", "large"

    def fee(self, hours):
        return 60 * hours


def install(registry):
    registry.add("bike", Bike())
    registry.add("car", Car())
    registry.add("truck", Truck())

fees/plugins/v1/receipt.py

def receipt(registry, vehicle, hours):
    kind = registry.get(vehicle)
    return f"{kind.label:<12} {kind.spot:<8} {hours} h  {kind.fee(hours):>4}"

Three choices keep this design honest:

  • The registry is an object, not a global dictionary. main.py creates it and hands it to whatever needs it, so no module reaches into shared state behind the others’ backs (the common-environment coupling of the first lesson in this module).
  • Each module of kinds installs itself. standard.install(registry) adds bike, car and truck. A module for a new kind follows the same pattern.
  • main.py is the single choice. It is the one place that knows the complete list of kinds, because it decides which modules to install.
Switch design: three modules list every vehicle kind. Registry design: kinds live in their own modules and follow one protocol.1. Switch statements2. A registry of vehicle typesmain.pyreceipt.pyLABELS for bike, car, truckfees.pyif bike / car / truckspots.pyif bike / car / truckmain.pyputs the kinds in the registrystandard.pyBike, Car, Truckev.py, the new fileElectricCarvehicles.py and receipt.pyknow only the VehicleType protocolinstall()install()follows the protocolfollows the protocol

Where a new kind of vehicle goes in each design

Text description of the diagram

The diagram shows the parking-fee program twice, one design above the other.

  1. Switch statements: main.py calls receipt.py, which keeps a LABELS dictionary for bike, car and truck and calls fees.py and spots.py, which each test for bike, car and truck in an if statement. A new kind of vehicle has to be added to all three modules.
  2. A registry of vehicle types: vehicles.py and receipt.py know only the VehicleType protocol, a label, a spot size and a fee(hours) method. standard.py holds Bike, Car and Truck, and ev.py, the new file, holds ElectricCar; both follow the protocol. main.py calls install() on each of them to put their kinds in the registry. Adding electric cars added ev.py and one install() line to main.py, and changed nothing in vehicles.py, receipt.py or standard.py.

Measuring the change

Both designs then got electric cars: a “charging” spot and the car rate plus 5 an hour for the charger. Each version is saved in its own folder, and this program compares them:

What adding electric cars changed Python · fees/measure_change.py
"""What adding electric cars changed in each design, and whether both designs print the same receipts.

Each design has two versions in its folder: v1, and v2 with electric cars added.
"""
import contextlib
import difflib
import io
import runpy
import sys
from pathlib import Path

HERE = Path(__file__).parent
DESIGNS = {"switch": "switch statements", "plugins": "registry of vehicle types"}


def run(folder):
    """What folder/main.py prints, with the folder's modules imported fresh."""
    for module in folder.glob("*.py"):
        sys.modules.pop(module.stem, None)
    sys.path.insert(0, str(folder))
    try:
        printed = io.StringIO()
        with contextlib.redirect_stdout(printed):
            runpy.run_path(str(folder / "main.py"), run_name="__main__")
        return printed.getvalue()
    finally:
        sys.path.remove(str(folder))


def lines(path):
    return path.read_text().splitlines() if path.exists() else []


sys.dont_write_bytecode = True
for version in ("v1", "v2"):
    same = run(HERE / "switch" / version) == run(HERE / "plugins" / version)
    print(f"{version}: both designs print the same receipts: {'yes' if same else 'NO'}")

print("\nAdding electric cars")
diffs = {}
for design, label in DESIGNS.items():
    old, new = HERE / design / "v1", HERE / design / "v2"
    edited, created, added, removed = [], [], 0, 0
    diffs[design] = []
    for name in sorted({p.name for p in old.glob("*.py")} | {p.name for p in new.glob("*.py")}):
        before, after = lines(old / name), lines(new / name)
        diff = list(difflib.unified_diff(before, after, f"v1/{name}", f"v2/{name}", n=0, lineterm=""))
        if not diff:
            continue
        (edited if (old / name).exists() else created).append(name)
        changed = [line for line in diff[2:] if not line.startswith("@@") and line[1:].strip()]
        added += sum(line.startswith("+") for line in changed)
        removed += sum(line.startswith("-") for line in changed)
        if (old / name).exists():
            diffs[design].append(diff)
    edited_text, created_text = ", ".join(edited) or "nothing", ", ".join(created) or "nothing"
    print(f"  {label}: edited {edited_text}; new {created_text}; +{added} -{removed} lines")

print("\nThe edits to existing files:")
for design, label in DESIGNS.items():
    print(f"\n[{label}]")
    for diff in diffs[design]:
        print("\n".join(diff))

Output

v1: both designs print the same receipts: yes
v2: both designs print the same receipts: yes

Adding electric cars
  switch statements: edited fees.py, main.py, receipt.py, spots.py; new nothing; +6 -2 lines
  registry of vehicle types: edited main.py; new ev.py; +12 -1 lines

The edits to existing files:

[switch statements]
--- v1/fees.py
+++ v2/fees.py
@@ -8,0 +9,2 @@
+    if vehicle == "ev":
+        return 30 + 20 * (hours - 1) + 5 * hours
--- v1/main.py
+++ v2/main.py
@@ -3 +3 @@
-for vehicle, hours in [("bike", 3), ("car", 2), ("truck", 1)]:
+for vehicle, hours in [("bike", 3), ("car", 2), ("truck", 1), ("ev", 2)]:
--- v1/receipt.py
+++ v2/receipt.py
@@ -4 +4 @@
-LABELS = {"bike": "Two-wheeler", "car": "Car", "truck": "Truck"}
+LABELS = {"bike": "Two-wheeler", "car": "Car", "truck": "Truck", "ev": "Electric car"}
--- v1/spots.py
+++ v2/spots.py
@@ -8,0 +9,2 @@
+    if vehicle == "ev":
+        return "charging"

[registry of vehicle types]
--- v1/main.py
+++ v2/main.py
@@ -0,0 +1 @@
+import ev
@@ -6,0 +8 @@
+ev.install(registry)
@@ -8 +10 @@
-for vehicle, hours in [("bike", 3), ("car", 2), ("truck", 1)]:
+for vehicle, hours in [("bike", 3), ("car", 2), ("truck", 1), ("ev", 2)]:

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 measure_change.py

Both designs added ("ev", 2) to the list of stays that main.py prints. Apart from that, the switch design edited three modules that already worked, fees.py, spots.py and receipt.py, and each edit is a chance to break bikes, cars or trucks. The registry design added one import and one install() call to main.py and put everything else in a new file, ev.py. It even added more lines than the switch design did. OCP does not promise less code; it promises that the tested code stays as it is, so the new code is the only place to look when something goes wrong.

Closed does not mean no edits anywhere

Something has to learn that electric cars exist. In this design it is main.py, the place where the program is put together. Python can move even that out of the code: an installed package can advertise a plugin as an entry point, declared in its pyproject.toml, and the program finds every plugin with importlib.metadata.entry_points() without naming any of them.

Text Diff Compare switch/v1/fees.py with switch/v2/fees.py, or any other pair of versions, line by line.

Other kinds of seam

A registry is one seam among several. Choose the smallest that does the job:

  • A function parameter: pass the varying behaviour in, as sorted(items, key=...) does. Good for one varying step.
  • A protocol or an abstract base class: callers depend on the shape, implementations vary. Good when a variant has several operations, as the vehicle types do.
  • A registry: a name-to-implementation table filled at start-up. Good when the variant is chosen by data, such as a vehicle code typed at the gate.
  • functools.singledispatch: a function whose implementation is chosen by the type of its first argument. New types register their own implementation with register(), without editing the function.

Closed sets and open sets

Not every set of variants should be open. Payment states (pending, paid, refunded) are fixed by the business, and new operations over them arrive all the time: describe a state, decide whether it can be refunded, choose the next actions. Here the opposite design pays off. Keep the states a closed set, a union of small data classes, and write each operation as one match that lists every state. typing.assert_never() marks the end of the list: a static type checker reports any match that misses a state, and at run time the call raises an exception.

A closed set of states, and a state nobody handled Python · states/payment_states.py
"""A closed set of payment states: every function over it lists every state, and assert_never() catches a gap.

Disputed was added to PaymentState later, and describe() was not updated for it.
"""
from dataclasses import dataclass
from typing import assert_never


@dataclass(frozen=True)
class Pending:
    amount: int


@dataclass(frozen=True)
class Paid:
    amount: int
    reference: str


@dataclass(frozen=True)
class Refunded:
    amount: int


@dataclass(frozen=True)
class Disputed:
    amount: int


type PaymentState = Pending | Paid | Refunded | Disputed


def can_refund(state: PaymentState) -> bool:
    match state:
        case Paid():
            return True
        case Pending() | Refunded() | Disputed():
            return False
        case _:
            assert_never(state)


def describe(state: PaymentState) -> str:
    match state:
        case Pending(amount):
            return f"waiting for {amount}"
        case Paid(amount, reference):
            return f"paid {amount} ({reference})"
        case Refunded(amount):
            return f"refunded {amount}"
        case _:
            assert_never(state)


states: list[PaymentState] = [Pending(500), Paid(500, "TXN-81"), Refunded(500), Disputed(500)]
for state in states:
    try:
        print(f"{describe(state):<24} refundable: {can_refund(state)}")
    except AssertionError as error:
        print(f"AssertionError: {error}")

Output

waiting for 500          refundable: False
paid 500 (TXN-81)        refundable: True
refunded 500             refundable: False
AssertionError: Expected code to be unreachable, but got: Disputed(amount=500)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 payment_states.py

Disputed was added to the union, can_refund() was updated and describe() was not. A static type checker reports that before the program runs. Without one, assert_never() still turns the gap into a clear AssertionError instead of a function that silently returns None.

Philip Wadler called this tension the expression problem. Think of the variants as rows of a table and the operations as columns. With an interface and one class per variant, adding a row is easy (a new class) and adding a column is hard (every class needs a new method). With a closed set and match, adding a column is easy (a new function) and adding a row is hard (every match needs a new case). Java offers the closed-set style through sealed classes, final in Java 17, and pattern matching for switch, which the compiler checks for exhaustiveness, final in Java 21.

Which way should the design be open?
Criterion A protocol, one class per variantA closed set and match
New variant A new class; no other code changesA new case in every match
New operation A new method in every classA new function; no other code changes
Who adds variants Anyone, even code you do not ownOnly the owner of the set
Tool help A type checker checks that each class has the methodsA type checker lists every match that misses a case
When to choose Vehicle types, payment providers, notification channelsPayment states, order statuses, the cells of a board game

When a seam is premature

A seam costs something: another file, another name, one more step between a caller and the code it runs. Martin Fowler’s note on yagni (“you aren’t gonna need it”) lists what a feature built for a presumed future costs. Building it is wasted effort if the need never comes, and even if it does come, the work delays features that are needed now and the extra code makes every later change harder until it pays off. He cites Jeremy Miller’s point that an unused extension point costs more than the effort spent on it, because it gets in the way. Fowler adds that yagni does not apply to work that makes code easier to change, such as refactoring; yagni only works because such work keeps code easy to change.

So this track’s rule of thumb is to add a seam when there is a reason in front of you:

  • a second variant exists or has been asked for, as the electric cars were (a fake that your tests need counts);
  • a rule changes often enough that each change risks the others (the reasons to change of the previous lesson);
  • other people must extend your code without editing it, as plugin authors do.

Until then, a plain if is fine, and turning it into a seam later is a small, safe refactoring when the tests are good.

In a design interview

Say where the seam goes even if you do not build it yet: “pricing is one function for now; when a second vehicle type with its own rule arrives, I will make fee a method of a VehicleType protocol”. Interviewers often follow up with exactly that new variant, and you can show the change touches one place.

Key takeaways

  • OCP: add a variant by writing new code behind a seam, and leave the tested code closed. Meyer’s version used inheritance; Martin’s uses interfaces and plugins.
  • Several switch statements over the same kinds break Meyer’s Single Choice principle; give the complete list to one module, such as the place where the program is put together.
  • An open set of variants suits a protocol and a registry; a closed set with many operations suits match and assert_never().
  • Add a seam when a second variant or a real reason to change appears, not for a future you imagine.

Exercise

Exercise · Easy · Python

Add two discount rules without editing Checkout

The shop's Checkout class in cart.py is finished and tested, and it is closed: it totals a cart and asks every rule it was given how much to take off. Each rule is an object with a method discount(lines, subtotal) that returns a number of cents. lines is a list of (name, unit price in cents, quantity) tuples.

Write the two rules the shop wants, in the classes waiting below Checkout:

  • BulkDiscount: 10% off every line of 10 or more units, rounded down to whole cents. Twelve pens at 250 cents each come to 3000 cents, so that line gets 300 off; nine pens get nothing.
  • BigOrder: 500 cents off when the subtotal is 5000 cents or more.

A shop then turns rules on by passing them in, for example Checkout([BulkDiscount(), BigOrder()]). Do not edit Checkout: the last sample test compares its code with the original, so any change to its code fails, even a reworded docstring. The other tests check totals with each rule, with both, and right at the edges, 9 or 10 units and a subtotal of 4999 or 5000 cents.

Starter code · cart.py

class Checkout:
    """Totals a cart and lets every rule it was given take an amount off. Closed: do not edit this class."""

    def __init__(self, rules=()):
        self.rules = list(rules)

    def total(self, lines):
        """lines: (name, unit price in cents, quantity) tuples."""
        subtotal = sum(price * quantity for _, price, quantity in lines)
        discount = sum(rule.discount(lines, subtotal) for rule in self.rules)
        return max(subtotal - discount, 0)


# Your discount rules go below this line, as new classes.


class BulkDiscount:
    """10% off every line of 10 or more units, rounded down to whole cents."""

    def discount(self, lines, subtotal):
        return 0


class BigOrder:
    """500 cents off an order whose subtotal is 5000 cents or more."""

    def discount(self, lines, subtotal):
        return 0
The sample tests · test_cart.py
import ast
import hashlib
from pathlib import Path

import cart
from cart import BigOrder, BulkDiscount, Checkout

CHECKOUT_SHA256 = "d35f5b8bf2795ec1d622e43be09149d57b7f7303ebc6a183800d226ff8280b68"


def test_no_rules():
    """without rules the total is the subtotal"""
    assert Checkout().total([("pens", 250, 12), ("notebook", 900, 1)]) == 3900


def test_bulk_discount():
    """takes 10% off lines of 10 or more units, rounded down"""
    checkout = Checkout([BulkDiscount()])
    assert checkout.total([("pens", 250, 12), ("notebook", 900, 1)]) == 3600
    assert checkout.total([("paper", 99, 11)]) == 981
    assert checkout.total([("rulers", 120, 10)]) == 1080
    assert checkout.total([("pens", 250, 9)]) == 2250


def test_big_order():
    """takes 500 off a subtotal of 5000 or more"""
    checkout = Checkout([BigOrder()])
    assert checkout.total([("stapler", 5000, 1)]) == 4500
    assert checkout.total([("stapler", 4999, 1)]) == 4999


def test_both_rules():
    """applies both rules to the same cart"""
    checkout = Checkout([BulkDiscount(), BigOrder()])
    assert checkout.total([("paper", 99, 11), ("stapler", 4500, 1)]) == 4981


def test_checkout_is_closed():
    """leaves the Checkout class exactly as it was"""
    tree = ast.parse(Path(cart.__file__).read_text(encoding="utf-8"))
    checkout = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "Checkout")
    digest = hashlib.sha256(ast.dump(checkout).encode()).hexdigest()
    assert digest == CHECKOUT_SHA256, "Checkout's code has changed: add discounts as new rule classes instead"
A hint

Each rule only looks at what it is given. BulkDiscount can add up price * quantity // 10 over the lines whose quantity is at least 10 (// rounds down), and BigOrder needs only subtotal.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 In Meyer's statement of the principle, what makes a module "closed"?

    Choose one answer.

    Show the answer to question 1

    Answer: It has a well-defined, stable interface, so other modules can already use it

    Closed means available for use: the module's interface is stable enough for clients to rely on, as a compiled library would be. Meyer adds that the principle is no excuse to leave bugs in place; a module with a flaw should be fixed.

  2. Question 2 of 6 Under Meyer's Single Choice principle, how many modules should know the complete list of vehicle kinds?

    Choose one answer.

    Show the answer to question 2

    Answer: Exactly one

    At least one module must know every kind, or the program could not offer them; at most one should, so that a new kind changes one place. In the registry design that module is main.py, which installs the kinds.

  3. Question 3 of 6 When electric cars were added, which existing files did the registry design edit?

    Choose one answer.

    Show the answer to question 3

    Answer: Only main.py, where the program is put together

    ev.py is new, and main.py gained an import and an install() call. vehicles.py, receipt.py and standard.py stayed as they were. fees.py, spots.py and receipt.py are the switch design's files, all three of which were edited.

  4. Question 4 of 6 Disputed is part of PaymentState, but describe() was not updated. What does payment_states.py print?

    What does this program print? Choose one answer.

    """A closed set of payment states: every function over it lists every state, and assert_never() catches a gap.
    
    Disputed was added to PaymentState later, and describe() was not updated for it.
    """
    from dataclasses import dataclass
    from typing import assert_never
    
    
    @dataclass(frozen=True)
    class Pending:
        amount: int
    
    
    @dataclass(frozen=True)
    class Paid:
        amount: int
        reference: str
    
    
    @dataclass(frozen=True)
    class Refunded:
        amount: int
    
    
    @dataclass(frozen=True)
    class Disputed:
        amount: int
    
    
    type PaymentState = Pending | Paid | Refunded | Disputed
    
    
    def can_refund(state: PaymentState) -> bool:
        match state:
            case Paid():
                return True
            case Pending() | Refunded() | Disputed():
                return False
            case _:
                assert_never(state)
    
    
    def describe(state: PaymentState) -> str:
        match state:
            case Pending(amount):
                return f"waiting for {amount}"
            case Paid(amount, reference):
                return f"paid {amount} ({reference})"
            case Refunded(amount):
                return f"refunded {amount}"
            case _:
                assert_never(state)
    
    
    states: list[PaymentState] = [Pending(500), Paid(500, "TXN-81"), Refunded(500), Disputed(500)]
    for state in states:
        try:
            print(f"{describe(state):<24} refundable: {can_refund(state)}")
        except AssertionError as error:
            print(f"AssertionError: {error}")
    Show the answer to question 4

    Answer: it prints

    waiting for 500          refundable: False
    paid 500 (TXN-81)        refundable: True
    refunded 500             refundable: False
    AssertionError: Expected code to be unreachable, but got: Disputed(amount=500)

    No case of describe() matches a Disputed, so the match falls through to case _ and assert_never() raises an AssertionError, which the loop prints. The other three states print as before; only Paid can be refunded.

  5. Question 5 of 6 Other teams add a new notification channel every few months. How should notify() change?

    Read the code, then choose one answer.

    def notify(user, message):
        if user.channel == "email":
            send_email(user.address, message)
        elif user.channel == "sms":
            send_sms(user.phone, message)
    Show the answer to question 5

    Answer: Give each channel a class with send(user, message) and look the channel up in a registry

    Channels are an open set that other people extend, and the operation is fixed: send a message. That is the case for a protocol and a registry, so a new channel is a new class. A closed union with match suits a set that only its owner changes and that gains operations, not variants.

  6. Question 6 of 6 Which of these suggest that adding a seam now would be premature?

    Choose every answer that is right.

    Show the answer to question 6

    Answer:

    • There is one variant and nobody has asked for a second
    • The new interface would have exactly one implementation, and neither the code nor its tests need a second one

    A seam built for a variant that does not exist adds a file and a layer you carry through every change. A second variant that has been asked for, or other people who must extend the code, are reasons in front of you, which is when this track adds the seam.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.