Concepts / Quality, testing and extensibility / Extensibility points

Extensibility points

Deciding in advance what will change, and leaving one door for it.

01The intuition

The problem it solves

A parking lot charges by vehicle type. The first version has the prices in the method that computes a fee. Then the interviewer says: weekends cost more. Then: electric cars get a discount. Each change edits the same method and the conditions pile up.

class ParkingLot:
    def fee(self, vehicle: str, hours: int, weekend: bool, electric: bool) -> int:
        rate = 20 if vehicle == "car" else 10 if vehicle == "bike" else 40
        if weekend:
            rate = int(rate * 1.5)
        if electric:
            rate -= 5
        return rate * hours

The method has become the place where every future business rule will land, and every rule risks the ones before it. The question an experienced designer asks is not "how do I add this rule" but "which kind of change will keep arriving, and where should it enter". That decision, made once, is an extensibility point.

Build it one piece at a time

Step one: name the thing that varies. Here it is "how a fee is computed", so make it an object with one method, and let the lot hold one. The lot no longer knows any prices.

from typing import Protocol

class Ticket:
    def __init__(self, vehicle: str, hours: int) -> None:
        self.vehicle, self.hours = vehicle, hours

class FeePolicy(Protocol):
    def fee(self, ticket: Ticket) -> int: ...

class FlatRate:
    RATES = {"car": 20, "bike": 10, "truck": 40}
    def fee(self, ticket: Ticket) -> int:
        return self.RATES[ticket.vehicle] * ticket.hours

class ParkingLot:
    def __init__(self, policy: FeePolicy) -> None:
        self.policy = policy

    def checkout(self, ticket: Ticket) -> int:
        return self.policy.fee(ticket)

Step two: new rules become new policies, or wrappers around an existing one. A weekend surcharge wraps any base policy; an electric discount wraps any policy too; they compose without touching each other or the lot.

class WeekendSurcharge:
    def __init__(self, base: FeePolicy, is_weekend: bool) -> None:
        self.base, self.is_weekend = base, is_weekend

    def fee(self, ticket: Ticket) -> int:
        f = self.base.fee(ticket)
        return int(f * 1.5) if self.is_weekend else f

class ElectricDiscount:
    def __init__(self, base: FeePolicy, electric: set[str]) -> None:
        self.base, self.electric = base, electric

    def fee(self, ticket: Ticket) -> int:
        f = self.base.fee(ticket)
        return max(0, f - 5 * ticket.hours) if ticket.vehicle in self.electric else f

lot = ParkingLot(ElectricDiscount(WeekendSurcharge(FlatRate(), is_weekend=True), {"car"}))

Step three: leave the door, not a hallway. One policy interface is the extension point; do not also make the ticket, the vehicle and the lot pluggable in case. The vehicle types stay an enum-like table, because a new vehicle is data, not behaviour. The design has one place for rule changes and one for data changes, and that is enough.

How each requested change lands after the refactor:

ChangeBeforeAfter
Weekend surchargeedit fee(), new flagnew WeekendSurcharge class
Electric discountedit fee(), new flagnew ElectricDiscount class
New vehicle typeedit fee()one row in RATES
Monthly passedit fee(), new flag, new branchnew policy wrapping any base

Why it works

The lot depends on an interface that promises "a fee for a ticket" and nothing more, so anything that keeps that promise can be substituted, including compositions of policies. That is the open-closed principle in practice: open to new policies, closed to edits of the lot. The point was chosen by looking at the first three change requests and noticing they were all of one kind.

Where it breaks: a point placed where change never comes is pure cost, and a point placed too late (after the fourth rule has already been hard-coded) means a refactor under pressure. The honest signal is the second request of the same kind. Another trap is leaking: if a policy needs to know about weekends, the ticket must carry the time, or the policy must be built with it; do not reach back into globals.

cost of the k-th rule: in the flat version it is k conditions to re-read and retest; with a policy point it is one new class and one test

The trade-off you must say out loud

Every extension point is an interface, a constructor argument and a little indirection a reader must follow. Add one where you can name the change that will use it, and say so: "I expect pricing rules to change, so pricing is a policy; I do not expect the ticket model to change, so it is a plain class." The follow-up that usually comes next is a change outside the point you chose, which is fine; say where it would go and what it would cost, rather than claiming the design absorbs everything.

Where it shows up in the 100 Google problems

02Used in these problems