Skip to content
FollowtheDamnCode

Smells: Change Preventers

Designs where one change forces many edits — one class changing for every reason, or one reason touching every class.

Big idea

Change preventers make the next change expensive. They come in mirror-image pairs: Divergent Change (one class edited for many unrelated reasons) and Shotgun Surgery (one reason forces small edits across many classes). Either way, a simple request turns into a risky, multi-file operation — so teams start avoiding changes, and the design ossifies.

The change preventers

SmellWhat it looks likeUsual fix
Divergent Change"Every time we add a payment method or change the report layout or switch databases, we edit OrderManager"Extract Class per reason (Single Responsibility)
Shotgun Surgery"Adding a currency means editing the formatter, the validator, the converter, the tax module, and the CSV export"Move Method / Move Field to gather the knowledge into one place (Moving Features)
Parallel Inheritance HierarchiesEvery new Shape subclass needs a matching ShapeRenderer subclassFold one hierarchy into the other, or use Visitor / Bridge deliberately

Think of it like

Divergent change is a single power socket that the kettle, the heater, and the washing machine all share — any of them blows the fuse for the others. Shotgun surgery is a house where changing the front-door lock means also changing a key on every family member's ring, the spare under the mat, and the one at the neighbour's — miss one and someone's locked out.

Shotgun surgery before and after: one concept, one home

Hands on: shotgun surgery

Adding Japanese yen required edits in three functions. Two got updated; the formatter still assumes two decimal places, so yen prints as 1500.00 JPY instead of 1500 JPY. Gather each currency's facts into one place so a new currency is one edit, and make the output match.

Fix it: a currency added in two of three places
def symbol(code):
    return {"EUR": "€", "USD": "$", "JPY": "¥"}[code]

def is_supported(code):
    return code in ("EUR", "USD", "JPY")

def format_amount(amount, code):
    return f"{symbol(code)}{amount:.2f} {code}"     # assumes 2 decimals everywhere


for amount, code in [(12.5, "EUR"), (3, "USD"), (1500, "JPY")]:
    if is_supported(code):
        print(format_amount(amount, code))
output
€12.50 EUR
$3.00 USD
¥1500 JPY

Run and edit it in the app.

Hands on: divergent change

Invoice mixes pricing, formatting, and persistence. A formatting change (show amounts with a thousands separator) was made by editing a shared helper that pricing also uses — and now the stored total is a string with a comma in it. Split the class so formatting can change without touching pricing or storage, keeping the output shown.

Fix it: a formatting change broke storage
class Invoice:
    def __init__(self, lines):
        self.lines = lines
        self.saved = {}

    def _money(self, x):
        return f"{x:,.2f}"                 # changed for display; pricing uses it too

    def total(self):
        return self._money(sum(q * p for q, p in self.lines))

    def render(self):
        return f"TOTAL {self.total()}"

    def save(self):
        self.saved["total"] = self.total()


inv = Invoice([(3, 400.0), (1, 999.5)])
print(inv.render())
inv.save()
print("stored:", inv.saved["total"], type(inv.saved["total"]).__name__)
output
TOTAL 2,199.50
stored: 2199.5 float

Run and edit it in the app.

Every new tax rule makes you edit TaxCalculator, InvoicePrinter, OrderValidator, and ReportExporter. Which smell?

Each time you add a Vehicle subclass you must also add a matching VehicleInsurance subclass. What's this called?

How it relates

Key terms

  • Divergent change — One class that keeps changing for several unrelated reasons.
  • Shotgun surgery — One change that requires many small edits across many classes.
  • Parallel inheritance hierarchies — Two class hierarchies where adding to one forces adding to the other.