Composing Methods — Find the Bug¶
12 snippets where a Composing Methods refactoring was applied incorrectly — the result looks refactored but breaks behavior, breaks performance, or introduces a subtle bug. Find the issue and fix it.
Bug 1 — Extract Method that captures the wrong scope (Java)¶
Original:
class Cart {
private List<Item> items;
private double discountRate;
double total() {
double sum = 0;
for (Item i : items) sum += i.price();
return sum * (1 - discountRate);
}
}
"Refactored":
class Cart {
private List<Item> items;
private double discountRate;
double total() {
return sum() * (1 - discountRate);
}
private static double sum() {
double s = 0;
for (Item i : items) s += i.price(); // ❌
return s;
}
}
Bug
`sum()` was made `static`. A static method can't access the instance field `items` — won't compile. The IDE wouldn't have made this mistake; a hand-typed extract did. **Fix:** Remove `static`.Bug 2 — Replace Temp with Query that double-counts side effects (Java)¶
Original:
double saleTax(Order o) {
double base = o.subtotal(); // pure
double tax = base * o.taxRate();
auditLog(o); // side effect
return base + tax;
}
"Refactored":
double saleTax(Order o) {
return base(o) + base(o) * o.taxRate();
}
private double base(Order o) {
auditLog(o); // ❌ moved into query
return o.subtotal();
}
Bug
`base()` is called twice in `saleTax`, so `auditLog(o)` fires twice. The audit log was a side effect that belonged in the orchestrator, not in a "query." **Fix:** A query method must be pure. Keep `auditLog(o)` outside. Or cache once:Bug 3 — Inline Method that breaks polymorphism (Java)¶
Original:
abstract class Shape {
abstract double area();
}
class Square extends Shape {
private double side;
@Override double area() { return side * side; }
}
Caller:
double totalArea(List<Shape> shapes) {
double t = 0;
for (Shape s : shapes) t += s.area();
return t;
}
"Refactored":
double totalArea(List<Shape> shapes) {
double t = 0;
for (Shape s : shapes) t += ((Square) s).side * ((Square) s).side; // ❌
return t;
}
Bug
Inlining inlined the `Square` implementation into the caller — but the loop receives `Shape`s, which may be `Circle`, `Triangle`, etc. The cast will throw `ClassCastException` for any non-Square. **Fix:** Don't inline polymorphic methods. Revert.Bug 4 — Split Temporary Variable that drops a use (Python)¶
Original:
def calc(h, w):
temp = 2 * (h + w)
print("perimeter", temp)
temp = h * w
print("area", temp)
return temp + 1
"Refactored":
def calc(h, w):
perimeter = 2 * (h + w)
print("perimeter", perimeter)
area = h * w
print("area", area)
return perimeter + 1 # ❌
Bug
The original returned `temp + 1` after `temp` was reassigned to `area`. So the original returned `area + 1`, not `perimeter + 1`. The refactor changed behavior. **Fix:** Lesson: When splitting a reassigned variable, *every* reference must be re-pointed to the *correct* split target.Bug 5 — Method Object loses callsite context (Java)¶
Original:
"Refactored":
class Gamma {
private int iv, q, ytd; // ❌ default 0
int compute() {
return (iv * q) + (iv * ytd) + 100;
}
}
class Account {
int gamma(int iv, int q, int ytd) {
return new Gamma().compute(); // ❌ never set fields
}
}
Bug
The Method Object was constructed without arguments — the fields are all zero. The result is always `100`. **Fix:** Pass via constructor.Bug 6 — Substitute Algorithm with edge case (Go)¶
Original:
"Refactored":
Bug
`slices.Max` panics on an empty slice — same as the original would (`xs[0]` panics). But the *test suite* may have set the contract that empty returns `0`. If that was a tacit invariant, the substitute changed observable behavior under contract. **Fix:** Check the contract. Or document explicitly that empty is undefined. Lesson: Substitute Algorithm must preserve **observable behavior**, including edge-case returns and panics.Bug 7 — Extract Variable that recomputes a side-effecting expression (Python)¶
Original:
"Refactored":
def assign_id(record, gen):
new_id = gen.next_id() # ❌ called even when id exists
if not record.id:
record.id = new_id
return record.id
Bug
The "extracted variable" `new_id = gen.next_id()` runs *before* the `if`, consuming a fresh ID even when one already exists. The id-generator's counter advances incorrectly. **Fix:** Don't extract a variable for an expression with side effects until the conditional clears.Bug 8 — Inline Temp where the temp had a type assertion (TypeScript)¶
Original:
"Refactored":
function area(s: Shape) {
return s.side * s.side; // ❌ Property 'side' does not exist on type 'Shape'
}
Bug
The temp `sq` was a TypeScript narrowing — the cast `as Square` told the compiler to treat the value as a Square. Inlining drops the cast and the code no longer compiles. **Fix:** Keep the cast inline. Or even better, narrow with a type guard:Bug 9 — Extract Method that introduces NPE (Java)¶
Original:
String fullDisplay(Customer c) {
if (c == null) return "guest";
return c.firstName() + " " + c.lastName();
}
"Refactored":
String fullDisplay(Customer c) {
if (c == null) return "guest";
return formatName(c);
}
private String formatName(Customer c) {
return c.firstName().toUpperCase() + " " + c.lastName().toUpperCase(); // ❌
}
Bug
The "extraction" wasn't a pure extraction — the developer added `.toUpperCase()` calls that change behavior. Also, `c.firstName()` may return null, and now `.toUpperCase()` will NPE. **Fix:** Extract first (behavior-preserving), then add `.toUpperCase()` as a separate change with tests. Lesson: a refactoring is **behavior-preserving by definition**. If you change behavior in the same step, it's not a refactor.Bug 10 — Replace Temp with Query that adds a query loop (Python)¶
Original:
def total(items):
sub = sum(i.price * i.qty for i in items)
if sub > 100: return sub * 0.9
return sub
"Refactored":
def total(items):
if subtotal(items) > 100: return subtotal(items) * 0.9
return subtotal(items)
def subtotal(items):
return sum(i.price * i.qty for i in items) # ❌ called 3×
Bug
`subtotal(items)` is called three times. For 100 items × 3 calls = 300 multiplications instead of 100. For a tight per-request hot path, this is a real regression. **Fix:** Cache once. The query is reusable from elsewhere; the orchestrator caches.Bug 11 — Remove Assignments to Parameters that breaks a returned value (Java)¶
Original:
String prefix(String s, int n) {
if (s.length() <= n) return s;
s = s.substring(0, n) + "...";
return s;
}
"Refactored":
String prefix(String s, int n) {
String result = s;
if (s.length() <= n) return s;
result = s.substring(0, n) + "...";
return s; // ❌
}
Bug
After introducing `result`, the assignment correctly went to `result`, but the final return still says `return s` — the original (unchanged) input. **Fix:** Return the right variable. Or, if you really want the temp:Bug 12 — Extract Method that obscures a return (Go)¶
Original:
func ProcessPayment(p Payment) error {
if p.Amount <= 0 {
return fmt.Errorf("invalid amount")
}
if !p.Card.IsValid() {
return fmt.Errorf("invalid card")
}
// ... charge logic ...
return nil
}
"Refactored":
func ProcessPayment(p Payment) error {
validate(p) // ❌
// ... charge logic ...
return nil
}
func validate(p Payment) error {
if p.Amount <= 0 { return fmt.Errorf("invalid amount") }
if !p.Card.IsValid() { return fmt.Errorf("invalid card") }
return nil
}
Bug
The error returned by `validate` is **discarded** — Go's compiler may not even warn (depends on lint settings). Validation now passes silently and the charge proceeds on invalid input. **Fix:** Propagate. Lesson: Extracting a method that returns an error means callers must handle the return value. The compiler can't always remind you.Common patterns¶
| Bug | Root cause |
|---|---|
| Static helper, instance field | Hand-typed extract didn't pull this |
| Side effect in "query" | Query must be pure |
| Inline polymorphic | Can't inline runtime dispatch |
| Split variable, missed reference | Need to update all uses |
| Method Object missing constructor | Fields default to zero |
| Algorithm substitute, edge case | Observable behavior includes edges |
| Extracted call discards return | Especially for error / Result |
Next¶
- Optimize: optimize.md
- Practice clean refactors: tasks.md
- Review: interview.md