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
| Smell | What it looks like | Usual 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 Hierarchies | Every new Shape subclass needs a matching ShapeRenderer subclass | Fold 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.
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.
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 JPYRun 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.
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 floatRun 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
- Divergent Change is low cohesion; Shotgun Surgery is knowledge spread too thin — often duplicated (DRY).
- Single Responsibility is the principle; Extract Class and Move Method (Moving Features) are the tools.
- Previous family: OO Abusers. Next: Dispensables.
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.