Simplifying Method Calls — Find the Bug¶
12 wrong refactors.
Bug 1 — Rename Method but missed a reflection call (Java)¶
Original:
public Money getCharge() { ... }
// Reflection caller:
Method m = c.getClass().getMethod("getCharge");
"Refactored":
public Money totalIncludingTax() { ... }
// Reflection caller still says "getCharge" — runtime NoSuchMethodError
Bug
IDE rename doesn't update string-based reflection or Spring SpEL or framework annotations. **Fix:** Search the codebase for the old name as a string. Update all references including configuration, JSON keys, etc.Bug 2 — Add Parameter without updating overrides (Java)¶
abstract class Validator { abstract boolean validate(Order o); }
class StrictValidator extends Validator { boolean validate(Order o) { ... } }
Refactor base: boolean validate(Order o, Context ctx).
But StrictValidator.validate(o) no longer overrides — it's a different signature. Compile error or hidden bug if no @Override.
Bug
When changing an abstract method, every subclass must update. IDE refactoring usually handles this; manual edits don't. **Fix:** Always use `@Override` annotation. The compiler then complains.Bug 3 — Separate Query from Modifier breaks atomicity (Java)¶
Original:
synchronized String getTotalAndSetReady() {
String t = computeTotal();
readyForSummary = true;
return t;
}
"Refactored":
Caller now does s.getTotal(); s.setReady(); — without synchronization, race condition if another thread is between.
Bug
The original was atomic; the refactor isn't. **Fix:** Either keep the combined method, or document that callers must serialize, or take a coarser lock at the caller. Lesson: separating Query from Modifier can break atomicity invariants.Bug 4 — Introduce Parameter Object reorders construction (Python)¶
Original:
"Refactored":
@dataclass
class DateRange:
start: date
end: date
def overlaps(a: DateRange, b: DateRange): ...
overlaps(DateRange(jan1, feb1), DateRange(jan31, feb28)) # ❌ wrong dates
Bug
Caller mistakenly grouped `(start_a, start_b)` together rather than `(start_a, end_a)` together. **Fix:** Group by *meaning* (range A's bounds together). Lesson: the parameter object's meaning must align with the caller's mental model.Bug 5 — Replace Constructor with Factory Method but caller serialization (Java)¶
Original:
public class Order {
public Order(String id, ...) { ... }
}
// Jackson reads JSON → calls constructor.
"Refactored":
public class Order {
private Order(String id, ...) { ... } // private
public static Order of(String id, ...) { ... }
}
Jackson can't deserialize — no public constructor.
Bug
Many serialization libraries require a public no-arg or annotated constructor. **Fix:** Or use Jackson's `@JsonCreator` on the factory method:Bug 6 — Replace Error Code with Exception in Go (Go)¶
// Original:
func Withdraw(amount float64) int {
if amount > balance { return -1 }
balance -= amount
return 0
}
// "Refactored":
func Withdraw(amount float64) {
if amount > balance { panic("insufficient") } // ❌
balance -= amount
}
Bug
Go convention is to return errors, not panic. `panic` is for unrecoverable programming errors. **Fix:** Lesson: each language has its own idiom for failure reporting; don't blindly translate.Bug 7 — Hide Method but tests still need it (Java)¶
Original:
Tests:
"Refactored": make internalRound private. Tests fail to compile.
Bug
Hiding a method that has tests breaks tests. **Fix options:** 1. Test through public interface only (preferred — tests aren't tied to internals). 2. Use package-private + same-package tests (Maven/Gradle conventions). 3. Use `@VisibleForTesting` annotation on a package-private method. Lesson: hide aggressively, but consider test seams.Bug 8 — Preserve Whole Object causes circular dependency (Java)¶
Original:
"Refactored":
class Plan {
boolean withinRange(TempRange tr) { return tr.getLow() ... } // Plan now depends on TempRange
}
If TempRange already depended on Plan (e.g., TempRange.fitsPlan(Plan p)), you've created a cycle.
Bug
Mutual dependency between Plan and TempRange. **Fix:** Either break the original direction, or pass primitives still. Lesson: Preserve Whole Object adds a dependency edge — check the graph before making the change.Bug 9 — Replace Parameter with Method Call calls expensive method (Java)¶
Original:
double tax(double base, double rate) { return base * rate; }
double total(Order o) {
double base = o.subtotal();
double rate = lookupRateExpensive(o.country());
return base + tax(base, rate);
}
"Refactored":
double tax(double base) { return base * lookupRateExpensive(country()); }
double total(Order o) {
double base = o.subtotal();
return base + tax(base);
}
Bug
`lookupRateExpensive` is now hidden inside `tax()`. If `total()` calls `tax()` more than once, the expensive lookup happens repeatedly. **Fix:** Cache or memoize. Or keep the parameter explicit:Bug 10 — Encapsulate Downcast hides a real type mismatch (Java)¶
Original:
Object o = readings.last();
if (o instanceof Reading) {
Reading r = (Reading) o;
process(r);
} else {
log.warn("unexpected type: " + o.getClass());
}
"Refactored":
Bug
The original handled the non-Reading case. The refactor throws ClassCastException for the same input. **Fix:** Either ensure the cast is always safe, or return Optional:Bug 11 — Builder allows invalid state (Java)¶
public class HttpRequest {
public static class Builder {
private String url;
private String method;
public Builder url(String u) { this.url = u; return this; }
public Builder method(String m) { this.method = m; return this; }
public HttpRequest build() { return new HttpRequest(url, method); }
}
}
new HttpRequest.Builder().build(); // ❌ url is null, method is null
Bug
Builder doesn't validate; you can construct an invalid request. **Fix:** Validate in `build()`: Or use a typed builder progression:Bug 12 — Replace Exception with Test introduces TOCTOU (Java)¶
Original:
"Refactored":
Bug
Time-of-check to time-of-use: between `contains` and `get`, another thread could evict the entry. `get` then throws. **Fix:** Atomic operation: Lesson: Replace Exception with Test must preserve atomicity. Use atomic APIs (computeIfAbsent, ConcurrentMap).Patterns¶
| Bug | Root cause |
|---|---|
| Reflection-based caller missed | IDE rename limited |
| Subclass override broken | No @Override annotation |
| Atomic operation split | Lost synchronization |
| Param object grouping wrong | Misaligned with mental model |
| Constructor private breaks deser | Need @JsonCreator |
| Wrong-language idiom | panic vs. error |
| Hidden method fails test | No test seam |
| Circular dep | Preserve Whole adds edge |
| Hidden expensive call | Replace Parameter calls more often |
| Encapsulated cast removed type guard | Lost null/type check |
| Invalid state in builder | No validation in build() |
| TOCTOU race | Atomicity lost in test-then-act |