Over-Engineering Anti-Patterns — Refactoring Practice¶
Category: Development Anti-Patterns → Over-Engineering — effort spent solving problems you don't have. Covers (collectively): Premature Optimization · Speculative Generality · Gold Plating · Yo-yo Problem · Lasagna Code · Accidental Complexity · Soft Coding · Bikeshedding
These are not "spot the smell" puzzles — find-bug.md does that. Here the code works, but it does too much: an interface with one implementation, a five-layer call chain, a rules engine standing in for an if, a loop hand-tuned without a profile. Your job is the opposite of every other topic's "optimize": here "optimize" almost always means simplify and remove, not speed up. You take over-engineered code and collapse it to the simplest thing that still passes the same tests.
The twist matters. In a few exercises you'll also see that the simpler code is as fast or faster — a benchmark sketch makes the point that abstraction isn't free: every interface dispatch, every layer hop, every config lookup is a cost the simple version never pays.
The discipline is identical to Bad Structure:
- Pin behavior first. Write a characterization test — a test that records what the code does now. You are freezing observable behavior, not blessing the design.
- Take small, reversible steps. One named refactoring at a time (Inline Function, Inline Class, Collapse Hierarchy, Remove Middle Man, …), tests green after each, commit.
- Separate structural from behavioral commits. Simplification must preserve behavior; if a test goes red, the last step is the culprit — revert it. A public API removal needs its own commit and a deprecation note.
Each worked solution names the canonical refactoring moves (Fowler's Refactoring, 2nd ed.) so you build the vocabulary, not just the instinct.
How to use this file: read the "Before" code, decide yourself what to remove and what to keep, write the move sequence down, then expand the solution. The hardest judgment call — exercises 3 and 5 — is what NOT to remove. A justified seam is not over-engineering; deleting it is under-engineering. Knowing the difference is the whole skill.
Table of Contents¶
| # | Exercise | Anti-pattern(s) | Lang | Key moves |
|---|---|---|---|---|
| 1 | Revert a premature optimization | Premature Optimization | Go | Substitute Algorithm; benchmark to confirm |
| 2 | Inline the speculative factory | Speculative Generality | Java | Inline Function, Inline Class, Remove Speculative Generality |
| 3 | Keep the seam that's justified | Speculative Generality (counter-case) | Java | Recognize a real seam; do NOT inline |
| 4 | Trim the gold-plated API | Gold Plating | Python | Remove Dead Parameter; deprecation note |
| 5 | Reduce accidental complexity (framework → direct) | Accidental Complexity | Python | Inline Class, Replace Pipeline Framework with Code |
| 6 | Collapse the Lasagna layers | Lasagna Code | Java | Remove Middle Man, Inline Class |
| 7 | Flatten Yo-yo inheritance to composition | Yo-yo Problem | Java | Replace Subclass with Delegate, Collapse Hierarchy |
| 8 | Migrate the soft-coded rule engine back to code | Soft Coding | Python | Replace Rule-Engine/Conditional with Code |
| 9 | Pre-decide the bikeshed | Bikeshedding | Go | Replace Hand-Rolled Style Knobs with a Tool default |
| 10 | Remove the dead flexibility (config that never varies) | Speculative Generality + Soft Coding | Go | Inline Parameter, Remove Dead Flexibility |
| 11 | Revert a cache nobody measured | Premature Optimization + Accidental Complexity | Go | Remove Speculative Cache; benchmark to confirm |
| 12 | Collapse the generic-repository tower | Speculative Generality + Lasagna | Java | Inline Class, Remove Middle Man, Collapse Hierarchy |
| 13 | The full combo simplification | All eight | Python | The whole toolbox, in order |
Exercise 1 — Revert a premature optimization¶
Anti-pattern: Premature Optimization. Goal: replace a cryptic hand-optimized routine with the clear idiomatic version, and prove with a benchmark that you lost no speed (here you actually gain some). Constraints: identical output for every input; no API change.
// Before — manual loop-unrolling + bit tricks on a routine that was never profiled.
func SumPositive(nums []int) int {
s, i, n := 0, 0, len(nums)
for ; i < n-3; i += 4 { // hand-unrolled by 4
if nums[i] > 0 {
s += nums[i]
}
if nums[i+1] > 0 {
s += nums[i+1]
}
if nums[i+2] > 0 {
s += nums[i+2]
}
if nums[i+3] > 0 {
s += nums[i+3]
}
}
for ; i < n; i++ { // tail
if nums[i] > 0 {
s += nums[i]
}
}
return s
}
Refactored
**Move sequence** 1. **Characterize.** Table test: empty slice, all-negative, all-positive, mixed, and a length not divisible by 4 (exercises the tail loop). Snapshot the returned sum. 2. **Write the benchmark first** — it's the entry ticket. You need before/after numbers to claim the simple version costs nothing:func BenchmarkSumPositive(b *testing.B) {
nums := makeData(1_000_000) // representative size
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = SumPositive(nums)
}
}
// $ go test -bench=SumPositive -benchmem -count=10 > new.txt
// compare old.txt new.txt with benchstat
Exercise 2 — Inline the speculative factory¶
Anti-pattern: Speculative Generality. Goal: collapse an interface + factory + strategy that has exactly one implementation, no test-double need, and no published-contract role. Constraints: preserve the greeting output; callers may be updated.
// Before — interface, factory, strategy, engine... to say "Hello, X".
interface GreetingStrategy {
String greet(String name);
}
class DefaultGreetingStrategy implements GreetingStrategy {
public String greet(String name) { return "Hello, " + name; }
}
class GreetingStrategyFactory {
static GreetingStrategy create() { return new DefaultGreetingStrategy(); }
}
class GreetingEngine {
private final GreetingStrategy strategy;
GreetingEngine(GreetingStrategy s) { this.strategy = s; }
String run(String name) { return strategy.greet(name); }
}
// Only call site anywhere in the codebase:
String msg = new GreetingEngine(GreetingStrategyFactory.create()).run("Sam");
Refactored
**Move sequence** 1. **Confirm it's speculative, not a seam.** Search for other implementations and for test doubles: One impl, never faked, not a published API → **Remove Speculative Generality** applies. (If any of those had hits, see Exercise 3 — you'd keep it.) 2. **Characterize.** A test asserting `greet("Sam") == "Hello, Sam"` through the current `GreetingEngine` path. 3. **Inline Function** — fold `DefaultGreetingStrategy.greet`'s body into a single method. 4. **Inline Class** — collapse `GreetingEngine`, the factory, and the strategy into one place; delete the interface and the factory. **What improved & how to verify.** Four types collapse to one method; the indirection a reader had to trace (engine → strategy → factory → impl) is gone. **Verify** with the characterization test — same output string. The deleted code is recoverable from git the day a *real* second greeting style appears, and you'll model it better against the actual requirement (YAGNI). Keep this a single **structural commit**. > **Cross-reference:** this is the textbook *Inline Class* + *Remove Speculative Generality* pair from [Refactoring → Techniques](../../../refactoring/02-refactoring-techniques/README.md). Compare with the next exercise, where the *same shape* is kept on purpose.Exercise 3 — Keep the seam that's justified (counter-case)¶
Anti-pattern: Speculative Generality — the counter-case. Goal: look at an interface-with-one-implementation that looks exactly like Exercise 2, and correctly decide to keep it. The skill here is restraint: not every abstraction is over-engineering, and inlining a real seam is under-engineering.
// Before — also one production implementation. Inline it too?
interface PaymentGateway {
Receipt charge(Money amount, Card card);
}
class StripeGateway implements PaymentGateway {
public Receipt charge(Money amount, Card card) {
return Stripe.charge(apiKey, card, amount.cents()); // real network call
}
}
class CheckoutService {
private final PaymentGateway gateway;
CheckoutService(PaymentGateway gateway) { this.gateway = gateway; }
Receipt checkout(Cart cart) {
return gateway.charge(cart.total(), cart.card());
}
}
Refactored
**Move sequence — the move is "stop, don't refactor"** 1. **Run the same speculation check as Exercise 2.** `grep` the implementations: one production impl (`StripeGateway`). So far identical to the greeting case. 2. **Now check the *justifications* for a seam** (from [`middle.md`](middle.md)). Any one is enough to keep it: | Justified-seam test | This code | |---|---| | Used as a **test double**? | **Yes** — `CheckoutServiceTest` injects a `FakePaymentGateway`; charging real cards in a unit test is impossible. | | A **volatile external dependency**? | **Yes** — Stripe is a third-party SDK; isolating it behind our own interface is deliberate. | | A **published contract / likely second impl**? | Plausible (PayPal). Not required — the first two already justify it. | 3. **Decision: keep the interface unchanged.** The only thing worth doing is *documenting why*, so a future reader doesn't "simplify" it by mistake:// After — unchanged code, plus the rationale that protects it from a wrong inline.
/**
* Seam is intentional, not speculative:
* - injected as a fake in CheckoutServiceTest (cannot charge real cards in unit tests)
* - isolates the volatile Stripe SDK behind our own boundary
* Do NOT inline. See over-engineering/optimize.md Ex.3.
*/
interface PaymentGateway {
Receipt charge(Money amount, Card card);
}
Exercise 4 — Trim the gold-plated API¶
Anti-pattern: Gold Plating. Goal: reduce a public function whose surface ballooned with options nobody asked for, down to its real, used surface — with a deprecation path for the public parameters you remove. Constraints: the two parameters that are actually used at call sites must keep working unchanged.
# Before — task was "save the report to a file". This is what shipped.
def save_report(report, path, *, fmt="txt", compress=False, encrypt=False,
email_to=None, watermark=None, theme="dark", retries=3,
upload_to_s3=False, callback=None):
text = report.render(theme=theme)
if watermark:
text = _apply_watermark(text, watermark) # never exercised
data = text.encode()
if compress:
data = gzip.compress(data) # never exercised
if encrypt:
data = _encrypt(data) # never exercised
_write_with_retries(path, data, retries)
if email_to:
_email(email_to, data) # never exercised
if upload_to_s3:
_s3_put(path, data) # never exercised
if callback:
callback(path) # never exercised
Refactored
**Move sequence** 1. **Establish the real surface.** Search every call site for which keyword args are ever passed:grep -rn "save_report(" --include="*.py" . | grep -oE "fmt=|compress=|encrypt=|email_to=|watermark=|theme=|retries=|upload_to_s3=|callback="
# result: only `theme=` and `retries=` ever appear.
# After — Step 4a: real surface kept; extras deprecated, not yet deleted.
import warnings
def save_report(report, path, *, theme="dark", retries=3, **deprecated):
if deprecated:
warnings.warn(
f"save_report() options {list(deprecated)} are deprecated and ignored; "
"removal in v3.0. See CHANGELOG.",
DeprecationWarning, stacklevel=2,
)
data = report.render(theme=theme).encode()
_write_with_retries(path, data, retries)
# After — Step 4b (v3.0): the surface the task actually required.
def save_report(report, path, *, theme="dark", retries=3):
data = report.render(theme=theme).encode()
_write_with_retries(path, data, retries)
Exercise 5 — Reduce accidental complexity (framework → direct)¶
Anti-pattern: Accidental Complexity. Goal: the problem is "uppercase a list of names." The code is a generic step-pipeline "framework." Replace the framework with the direct expression. Constraints: same output list; same order.
# Before — a reusable "transformation pipeline" engine for a one-line map().
from abc import ABC, abstractmethod
class Step(ABC):
@abstractmethod
def apply(self, data): ...
class UpperStep(Step):
def apply(self, data): return [x.upper() for x in data]
class StripStep(Step):
def apply(self, data): return [x.strip() for x in data]
class Pipeline:
def __init__(self, steps): self.steps = steps
def run(self, data):
for step in self.steps:
data = step.apply(data)
return data
# the only call site:
clean = Pipeline([StripStep(), UpperStep()]).run(raw_names)
Refactored
**Move sequence** 1. **Describe the essential problem in one sentence.** "Strip whitespace and uppercase each name." If the code is dramatically larger than that sentence, the gap is accidental complexity. 2. **Confirm the framework has one client.** `grep -rn "Pipeline(" .` → one call site, two steps, never extended. The `Step` ABC, polymorphism, and `Pipeline` runner serve no real variation. 3. **Characterize.** Test the call site's output for `[" ann ", "BOB"]` → `["ANN", "BOB"]`. 4. **Inline Function** each step's body, then **Inline Class** the pipeline — replace the whole machine with the direct comprehension. **What improved & how to verify.** An ABC, two `Step` classes, a `Pipeline` runner, and dynamic dispatch collapse to one comprehension a reader understands instantly. **Verify** with the characterization test — identical list. As a bonus, **abstraction isn't free**: the framework did two list allocations (one per step) plus a virtual `apply` call per step; the comprehension does it in one pass. A quick `timeit` confirms the direct version is faster *and* shorter: > **The umbrella pattern:** accidental complexity is what Speculative Generality, Soft Coding, and Lasagna all *produce*. Removing the framework here is the same instinct you'll apply to the rules engine (Ex. 8) and the layers (Ex. 6).Exercise 6 — Collapse the Lasagna layers¶
Anti-pattern: Lasagna Code. Goal: collapse a chain of pass-through layers that each only forward the call, keeping the one layer that has a real responsibility. Constraints: preserve OrderApi.getOrder's behavior; callers unchanged.
// Before — five layers; only one does real work.
class OrderApi {
private final OrderService service;
OrderApi(OrderService s) { this.service = s; }
Order getOrder(int id) { return service.getOrder(id); } // forwards
}
class OrderService {
private final OrderManager manager;
OrderService(OrderManager m) { this.manager = m; }
Order getOrder(int id) { return manager.fetch(id); } // forwards (renames)
}
class OrderManager {
private final OrderHandler handler;
OrderManager(OrderHandler h) { this.handler = h; }
Order fetch(int id) { return handler.handle(id); } // forwards (renames)
}
class OrderHandler {
private final OrderRepository repo;
OrderHandler(OrderRepository r) { this.repo = r; }
Order handle(int id) {
Order o = repo.load(id);
if (o == null) throw new NotFoundException(id); // REAL job: not-found policy
return o;
}
}
class OrderRepository {
Order load(int id) { return db.queryOne("SELECT ... WHERE id=?", id); } // REAL job: the query
}
Refactored
**Move sequence** 1. **Audit each layer for a distinct responsibility.** `OrderApi`, `OrderService`, `OrderManager` only forward (some rename the arg) — pass-throughs. `OrderHandler` owns the *not-found policy*. `OrderRepository` owns the *query*. Two real jobs; three empty layers. 2. **Characterize.** Test `getOrder` for a found id (returns the order) and a missing id (throws `NotFoundException`). Mock the DB. 3. **Remove Middle Man** (Fowler) / **Inline Class** the three forwarders one at a time, bottom-up, tests green after each. Keep the not-found policy and the query.// After — two layers, each with a real responsibility.
class OrderRepository {
Order findOrThrow(int id) {
Order o = db.queryOne("SELECT ... WHERE id=?", id); // query
if (o == null) throw new NotFoundException(id); // not-found policy
return o;
}
}
class OrderApi { // public entry point preserved
private final OrderRepository repo;
OrderApi(OrderRepository repo) { this.repo = repo; }
Order getOrder(int id) { return repo.findOrThrow(id); }
}
Exercise 7 — Flatten Yo-yo inheritance to composition¶
Anti-pattern: Yo-yo Problem. Goal: to learn what notify() does you must climb a four-level inheritance chain with super calls weaving up and down. Replace the inheritance tower with composition so behavior lives in one place. Constraints: preserve the message each notifier produces.
// Before — behavior smeared across four levels; super calls yo-yo up and down.
abstract class Notifier {
String notify(String msg) { return decorate(msg); }
protected String decorate(String msg) { return msg; }
}
class TimestampedNotifier extends Notifier {
protected String decorate(String msg) { return "[" + now() + "] " + super.decorate(msg); }
}
class TaggedNotifier extends TimestampedNotifier {
protected String decorate(String msg) { return super.decorate("#alert " + msg); }
}
class UpperNotifier extends TaggedNotifier {
protected String decorate(String msg) { return super.decorate(msg).toUpperCase(); }
}
// To answer "what does new UpperNotifier().notify('hi') return?" you climb 4 files.
Refactored
**Move sequence** 1. **Characterize.** Pin the output of the concrete classes actually instantiated: `new UpperNotifier().notify("hi")`, `new TaggedNotifier().notify("hi")`, etc. Record the exact strings — `super`-chains make the order subtle, so the test is essential. 2. **Replace Subclass with Delegate** (Fowler): model each `decorate` behavior as a small composable `Transform` (a function), and build a notifier by *listing* the transforms it applies — no inheritance. 3. **Collapse Hierarchy** — delete the four-class tower once the composed version reproduces the snapshots.// After — each behavior is one named transform; composition lists them in order.
@FunctionalInterface
interface Transform { String apply(String s); }
class Notifier {
private final List<Transform> steps;
Notifier(Transform... steps) { this.steps = List.of(steps); }
String notify(String msg) {
String out = msg;
for (Transform t : steps) out = t.apply(out);
return out;
}
}
// Named, reusable transforms — behavior lives in one obvious place:
Transform tag = s -> "#alert " + s;
Transform timestamp = s -> "[" + now() + "] " + s;
Transform upper = String::toUpperCase;
// The old UpperNotifier, expressed as the exact pipeline it really was:
Notifier upperNotifier = new Notifier(tag, timestamp, upper);
Exercise 8 — Migrate the soft-coded rule engine back to code¶
Anti-pattern: Soft Coding. Goal: business logic has been encoded as a JSON "rule engine" interpreted at runtime — untyped, untestable, undebuggable. Migrate the rules back into tested code. Constraints: identical eligibility decision for every input; the migration must be provably behavior-preserving.
# Before — discount logic lives in JSON, run by a hand-rolled interpreter.
RULES = [
{"if": {"field": "tier", "op": "==", "val": "vip"},
"and": {"field": "total", "op": ">=", "val": 100},
"then": {"discount": 0.15}},
{"if": {"field": "region", "op": "in", "val": ["EU", "UK"]},
"and": {"field": "total", "op": ">=", "val": 50},
"then": {"discount": 0.10}},
{"else": {"discount": 0.0}},
]
def discount(order):
for rule in RULES:
if "else" in rule:
return rule["else"]["discount"]
if _match(order, rule["if"]) and _match(order, rule["and"]):
return rule["then"]["discount"]
return 0.0
def _match(order, cond): # a tiny, untyped expression interpreter
actual = getattr(order, cond["field"])
op = cond["op"]
if op == "==": return actual == cond["val"]
if op == ">=": return actual >= cond["val"]
if op == "in": return actual in cond["val"]
raise ValueError(f"unknown op {op}")
Refactored
**Move sequence** 1. **Confirm it's soft-coded *logic*, not config.** The JSON contains operators (`==`, `>=`, `in`) and control flow (`if`/`and`/`else`) — it's a programming language reinvented in data. (Contrast: a single `vip_discount_rate = 0.15` value *would* be fine in config.) Confirm non-developers don't actually edit `RULES` (check git history of the file — if every change is a developer PR, the "so business can edit it" justification is false). 2. **Characterize against the interpreter.** This is the safety net for the whole migration. Generate cases across the decision space and snapshot the engine's output: 3. **Replace Conditional/Rule-Engine with Code** — transcribe each rule into a plain, ordered `if`. Keep the *same order* the engine evaluated (first match wins). 4. **Delete the interpreter and the JSON** once the new function reproduces `golden` exactly. **What improved & how to verify.** The logic is now type-checked, steppable in a debugger, and covered by real unit tests; the bespoke `_match` interpreter (a place bugs hid invisibly) is deleted. **Verify** with a *parallel-run characterization test*: assert `new_discount(c) == golden[c]` for every case — if all match, the migration is behavior-preserving. Keep the rule *order* identical (first-match-wins), the one place a transcription bug would slip in. > **If business truly must edit rules without a deploy** (a *proven* need, not a reflex): that's a deliberate feature — a *narrow, validated* table with no operators, plus tests — not a hand-rolled untyped interpreter. See the config-vs-code spectrum in [`middle.md`](middle.md).Exercise 9 — Pre-decide the bikeshed¶
Anti-pattern: Bikeshedding. Goal: a project hand-rolled a configurable "style" layer so every formatting choice can be argued and toggled. Replace the whole apparatus with a tool's default, so the trivial choice is pre-decided and undebatable. Constraints: output formatting may change to the tool's canonical form (that's the point); logic unchanged.
// Before — a homegrown formatter with knobs for every style preference,
// each of which spawned a 40-comment PR thread.
type FormatOptions struct {
IndentWithTabs bool
IndentWidth int
BraceOnNewLine bool
MaxLineLength int
QuoteStyle string // "single" | "double"
TrailingComma bool
}
func Format(src string, opts FormatOptions) string {
// 200 lines re-implementing, configurably, what gofmt already does
// ...
}
Refactored
**Move sequence** 1. **Recognize the meta-pattern.** Every field of `FormatOptions` is a *trivial, reversible* choice that the team has spent disproportionate effort debating. Bikeshedding thrives on accessible problems; the cure is to *remove the accessible problem*. 2. **Adopt the canonical tool.** The language already ships an opinionated formatter with no options — `gofmt`. Replace the homegrown formatter and its option struct with a single call to the standard tool; for Go, that's `go/format`: 3. **Wire it into CI** so the choice is enforced, not discussed: **What improved & how to verify.** Two hundred lines of configurable formatter and an entire category of PR debate (tabs vs spaces, brace placement, quote style) disappear — the formatter has *no options to argue about*, and CI enforces it so the topic never returns to a review. **Verify** the logic that *used* the formatter still behaves the same; the formatting output deliberately changes to the canonical style, which is the intended improvement, not a regression. **The broader move:** automate the trivial out of existence (`gofmt`, Prettier, Black, a linter), reserve human review attention for decisions with real blast radius.Exercise 10 — Remove the dead flexibility (config that never varies)¶
Anti-pattern: Speculative Generality + Soft Coding. Goal: a function is drowning in "configurable" parameters that every caller passes with the same value. Inline the constants and remove the dead flexibility. Constraints: preserve output for the values actually in use; no caller passes anything different.
// Before — "flexible" retry helper; every call site passes identical knobs.
func FetchWithPolicy(
url string,
maxRetries int,
backoffBase time.Duration,
backoffFactor float64,
jitter bool,
timeout time.Duration,
retryOn func(int) bool,
) ([]byte, error) {
// honors every parameter...
}
// All 12 call sites, verbatim:
FetchWithPolicy(u, 3, 100*time.Millisecond, 2.0, true, 5*time.Second,
func(code int) bool { return code >= 500 })
Refactored
**Move sequence** 1. **Prove the flexibility is dead.** Audit every call site for variation:grep -rn "FetchWithPolicy(" --include="*.go" .
# all 12 calls pass: 3, 100ms, 2.0, true, 5s, and the same >=500 predicate.
// After — the policy that actually exists, named once. Knobs return only when a real need does.
const (
maxRetries = 3
backoffBase = 100 * time.Millisecond
timeout = 5 * time.Second
)
func Fetch(url string) ([]byte, error) {
// exponential backoff (base 100ms, ×2) + jitter, retry on 5xx, 5s timeout — inlined
// ...
}
// call site:
Fetch(u)
Exercise 11 — Revert a cache nobody measured¶
Anti-pattern: Premature Optimization + Accidental Complexity. Goal: a memoization cache was bolted onto a cheap pure function "for performance," adding mutexes and a map but no measured benefit. Remove it and benchmark to confirm the simple version is as fast (often faster, since the cache had locking overhead). Constraints: identical results; thread-safe (the function is called concurrently).
// Before — a concurrency-safe cache wrapped around a trivial computation.
type SquareCache struct {
mu sync.Mutex
cache map[int]int
}
func NewSquareCache() *SquareCache {
return &SquareCache{cache: make(map[int]int)}
}
func (c *SquareCache) Square(n int) int {
c.mu.Lock()
defer c.mu.Unlock()
if v, ok := c.cache[n]; ok {
return v
}
v := n * n
c.cache[n] = v
return v
}
Refactored
**Move sequence** 1. **Question the optimization.** Caching is justified when the cached computation is *expensive* and inputs *repeat*. Here the computation is a single multiply — cheaper than a map lookup, far cheaper than a mutex acquire. There is no profile showing `Square` was ever a bottleneck. This is premature optimization that also added accidental complexity (shared mutable state + locking). 2. **Characterize.** Test `Square(0)`, `Square(7)`, `Square(-3)` → `0, 49, 9`. 3. **Write a benchmark for both versions**, including a *concurrent* benchmark — the lock's cost only shows under contention:func BenchmarkSquareCacheParallel(b *testing.B) {
c := NewSquareCache()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() { _ = c.Square(7) }
})
}
func BenchmarkSquareParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() { _ = Square(7) }
})
}
Exercise 12 — Collapse the generic-repository tower¶
Anti-pattern: Speculative Generality + Lasagna. Goal: a generic Repository<T> abstraction with an abstract base, a generic interface, and a per-entity subclass exists to serve one entity with one query. Collapse the tower to the concrete code that's actually used. Constraints: preserve findUser's behavior; the only consumer may be updated.
// Before — a four-type generic-repository framework serving exactly one entity.
interface Repository<T, ID> {
Optional<T> findById(ID id);
List<T> findAll();
T save(T entity);
void deleteById(ID id);
}
abstract class AbstractRepository<T, ID> implements Repository<T, ID> {
protected abstract String table();
protected abstract T map(ResultSet rs) throws SQLException;
public Optional<T> findById(ID id) { /* generic reflective query */ }
public List<T> findAll() { /* generic */ } // never called
public T save(T entity) { /* generic reflective insert */ } // never called
public void deleteById(ID id) { /* generic */ } // never called
}
class UserRepository extends AbstractRepository<User, Long> {
protected String table() { return "users"; }
protected User map(ResultSet rs) throws SQLException {
return new User(rs.getLong("id"), rs.getString("email"));
}
}
// The only call anywhere:
Optional<User> u = userRepository.findById(42L);
Refactored
**Move sequence** 1. **Measure the gap between the framework and its use.** `grep` the consumers: only `findById` is ever called, only for `User`. `findAll`/`save`/`deleteById` have zero callers; the generic machinery (reflection, type params, abstract `table()`/`map()`) serves a single concrete case. This is Speculative Generality stacked into a Lasagna of three types where one would do. 2. **Characterize.** Test `findById(42L)` → the expected `User`; `findById(unknown)` → `Optional.empty()`. Mock the DB. 3. **Remove Speculative Generality** — delete the three unused interface methods and their generic implementations. 4. **Collapse Hierarchy** + **Inline Class** — fold `AbstractRepository` and the `Repository` interface into the one concrete `UserRepository`, replacing the reflective query with a plain, readable SQL call.// After — the one query that exists, written plainly.
class UserRepository {
private final DataSource db;
UserRepository(DataSource db) { this.db = db; }
Optional<User> findById(long id) {
User u = db.queryOne(
"SELECT id, email FROM users WHERE id = ?", id,
rs -> new User(rs.getLong("id"), rs.getString("email")));
return Optional.ofNullable(u);
}
}
Exercise 13 — The full combo simplification¶
Anti-pattern: all eight at once. Goal: one module exhibits a premature optimization, a speculative interface, a gold-plated option, a soft-coded rule, accidental framework complexity, a Lasagna hop, and a bikeshed-able knob. Simplify it in a safe order. Constraints: preserve the computed total; tackle the patterns from safest/most-isolated to most-entangled.
# Before — a "configurable, extensible, optimized" order-total calculator.
from abc import ABC, abstractmethod
# soft-coded tax rule (operators in data) + gold-plated knobs
CONFIG = {
"rounding": "half_up", # only "half_up" ever used
"currency_symbol": "$", # bikeshed magnet; logic doesn't need it
"tax_rules": [{"if": {"field": "region", "op": "==", "val": "US"}, "then": 0.08},
{"else": 0.0}],
}
class TotalStep(ABC): # speculative framework for two steps
@abstractmethod
def apply(self, ctx): ...
class SubtotalStep(TotalStep):
def apply(self, ctx):
# premature optimization: manual unroll of a sum
items, s, i, n = ctx["items"], 0.0, 0, len(ctx["items"])
while i < n - 1:
s += items[i].price * items[i].qty
s += items[i + 1].price * items[i + 1].qty
i += 2
while i < n:
s += items[i].price * items[i].qty
i += 1
ctx["subtotal"] = s
return ctx
class TaxStep(TotalStep): # interprets the soft-coded rules
def apply(self, ctx):
rate = 0.0
for rule in CONFIG["tax_rules"]:
if "else" in rule: rate = rule["else"]; break
if getattr(ctx["order"], rule["if"]["field"]) == rule["if"]["val"]:
rate = rule["then"]; break
ctx["tax"] = ctx["subtotal"] * rate
return ctx
class Pipeline: # Lasagna: a runner that just loops
def __init__(self, steps): self.steps = steps
def run(self, ctx):
for s in self.steps: ctx = s.apply(ctx)
return ctx
def order_total(order): # speculative seam: one impl, never mocked
ctx = {"order": order, "items": order.items}
ctx = Pipeline([SubtotalStep(), TaxStep()]).run(ctx)
return round(ctx["subtotal"] + ctx["tax"], 2)
Refactored
**Move sequence — safest, most-isolated first** 1. **Characterize.** Table test across `region in {US, EU}` × `{empty cart, one item, several items, odd item count}`; snapshot the returned total. This single test guards the entire simplification. 2. **Revert the premature optimization.** The manual unrolled sum in `SubtotalStep` is unmeasured and obscures a one-line sum. **Substitute Algorithm** → `sum(i.price * i.qty for i in items)`. (Benchmark to confirm the comprehension is as fast — it is.) Tests green. 3. **Migrate the soft-coded tax rule back to code.** The `tax_rules` JSON is a tiny interpreter; transcribe it to a plain `if`, preserving first-match order. Delete the interpreter loop and the `tax_rules` config. 4. **Remove the gold-plated / bikeshed knobs.** `rounding` has one value and `currency_symbol` isn't used by the calculation — **Inline Parameter** / delete from `CONFIG`. (If `currency_symbol` is needed for *display*, that belongs in the formatting layer, not the total calculation — capture as a separate ticket.) 5. **Remove the accidental-complexity framework + Lasagna runner.** With two trivial steps, the `TotalStep` ABC and the `Pipeline` runner serve no variation — **Inline Class** both into a plain function. 6. **Inline the speculative seam.** `order_total` had no second implementation and was never mocked → keep it as a plain function (no interface). (If it *were* mocked in tests, you'd keep a thin seam, per Ex. 3 — it isn't.)# After — the essential calculation, stated directly.
def subtotal(order) -> float:
return sum(item.price * item.qty for item in order.items)
def tax(order, subtotal_amount: float) -> float:
rate = 0.08 if order.region == "US" else 0.0
return subtotal_amount * rate
def order_total(order) -> float:
sub = subtotal(order)
return round(sub + tax(order, sub), 2)
Simplification discipline — the recap¶
Every exercise above runs the same loop — note that "the change" here is almost always a removal:
green → one named removal/inline → green → commit → repeat
(a public-API removal gets its OWN commit + deprecation note)
- Characterize before you simplify. A test that pins current observable behavior is the seatbelt; for a logic migration (Ex. 8, 13) a parallel run comparing old output to new is the characterization test. Without it, "simplifying" is just deleting and hoping.
- Small, reversible steps. Inline one class, run tests, commit. If green turns red, your last removal is the suspect — revert it. Git is the undo button, so deletion is cheap and recoverable the day a real need appears (YAGNI in action).
- Separate structural from behavioral commits. Simplification must preserve behavior; the canonical-formatter switch (Ex. 9) and any rounding change (Ex. 13) are deliberate behavioral deltas — give them their own reviewed commits, never smuggled into a structural one.
- Know what NOT to remove. A test-double seam, a volatile-dependency boundary, or a published contract is justified — inlining it is under-engineering (Ex. 3, and the "keep the seam" notes in Ex. 12). The shape (interface + one impl) never decides; the justification does.
- Public removals need a deprecation path. Trimming a gold-plated public API (Ex. 4) ships as deprecate-then-delete across releases with a
DeprecationWarningand a CHANGELOG entry — never a silent breaking delete. - Benchmark before claiming "faster." When you assert the simpler version costs nothing (Ex. 1, 5, 11), prove it with
benchstat/timeitnumbers. The recurring finding: abstraction isn't free — unrolling the compiler already does, caches that only add lock contention, and frameworks that add allocations all make the simple version as fast or faster.
| Move | Cures | Exercises |
|---|---|---|
| Substitute Algorithm / Remove Speculative Cache (+ benchmark) | Premature Optimization | 1, 11, 13 |
| Inline Function / Inline Class / Remove Speculative Generality | Speculative Generality | 2, 5, 10, 12, 13 |
| Recognize & keep a justified seam | Speculative Generality (counter-case) | 3, 12 |
| Remove Dead Parameter (+ deprecation note) | Gold Plating | 4, 13 |
| Inline Class / Replace Framework with Direct Code | Accidental Complexity | 5, 11, 13 |
| Remove Middle Man / Inline Class | Lasagna Code | 6, 12 |
| Replace Subclass with Delegate / Collapse Hierarchy | Yo-yo Problem | 7 |
| Replace Rule-Engine/Conditional with Code (+ parallel-run) | Soft Coding | 8, 13 |
| Replace Hand-Rolled Knobs with a Tool Default | Bikeshedding | 9, 13 |
| Inline Parameter / Remove Dead Flexibility | Speculative Generality + Soft Coding | 10, 13 |
Related Topics¶
tasks.md— guided exercises building these simplification moves from scratch.find-bug.md— the spotting counterpart: identify the over-engineering, don't fix it.junior.md·middle.md·senior.md— recognize → calibrate the right altitude → simplify at scale.- Bad Structure → optimize — the same refactoring discipline applied to under-structured code (the over-correction to avoid when collapsing layers is Spaghetti).
- Bad Shortcuts → middle — the Rule of Three and the wrong-abstraction trap; Soft Coding as the over-correction of Hard Coding.
- Refactoring → Techniques — the mechanical catalog for Inline Class, Inline Function, Collapse Hierarchy, Replace Subclass with Delegate, Remove Middle Man.
- Refactoring → Code Smells — Speculative Generality, Middle Man, and Lazy Element at the smell level.
- Clean Code → Classes — composition over inheritance; cohesion, the target state for Exercise 7.
In this topic