Dealing with Generalization — Optimize¶
12 cases where the refactor is correct but introduces a perf cost.
Optimize 1 — Pull Up Method to abstract creates virtual call (Java)¶
abstract class Employee { abstract double pay(); }
class Engineer extends Employee { double pay() { return 5000; } }
class Manager extends Employee { double pay() { return 7000; } }
double total = list.stream().mapToDouble(Employee::pay).sum(); // virtual call per element
Cost & Fix
For monomorphic call site (only Engineers), JIT inlines. Bimorphic: still fast. Megamorphic (4+ types): falls back to vtable. **Fix:** Mark `Employee` `sealed` (Java 17+): JIT knows the closed set; can devirtualize completely. For pre-Java 17: mark methods `final` if they shouldn't be overridden in deeper subclasses.Optimize 2 — Form Template Method makes hot loop slow (Java)¶
abstract class Statement {
public final String emit(Customer c) {
return header() + lines(c) + footer();
}
protected abstract String header();
protected abstract String lines(Customer c);
protected abstract String footer();
}
In a batch generating 10K statements: 30K virtual calls.
Cost & Fix
If only one Statement type is used in this batch, monomorphic — JIT inlines. ~zero cost. If multiple types: bimorphic still fine. Megamorphic costs vtable lookups. **Fix:** 1. Sealed types — closed set, JIT specializes. 2. Specialized batch processors (each Statement type gets its own batch). 3. Profile with `-XX:+PrintInlining` to confirm inlining happens.Optimize 3 — Replace Inheritance with Delegation adds dereference (Java)¶
class Stack<E> {
private final Vector<E> data;
public void push(E e) { data.add(e); } // 1 extra deref
}
In a hot loop pushing 1M items.
Cost & Fix
Each `push` dereferences `data` then calls `add`. JIT inlines, eliminating the deref. In steady state: zero cost. If `data` field is volatile or otherwise can't be cached by JIT: small overhead. **Fix:** No fix needed in typical case. For very hot paths: profile.Optimize 4 — Pull Up Field carries waste in subclasses (Java)¶
abstract class Employee {
protected double quota; // only Salesman uses
protected int level; // only Engineer uses
protected int grade; // only Manager uses
}
Every Employee instance now has 3 unused fields most of the time.
Cost & Fix
For 10M employees, each carries 24 unused bytes. 240 MB wasted. **Fix:** Push these fields down to specific subclasses where they belong. Pull Up was wrong.Optimize 5 — Extract Interface introduces interface dispatch (Go)¶
type Greeter interface { Greet() string }
func process(g Greeter) { fmt.Println(g.Greet()) } // virtual call
for _, e := range employees {
process(e) // interface dispatch
}
Cost & Fix
Each `g.Greet()` is an itable lookup + indirect call. ~3-5 cycles vs. direct call (~1 cycle). **Fix:** 1. **Generics (Go 1.18+):** Compiler instantiates per type — direct call. 2. **PGO (Go 1.21+):** devirtualize hot interface calls. 3. **Concrete types in hot loops:** if you only have one type, don't use the interface.Optimize 6 — Sealed types still megamorphic (Java)¶
10 cases. Pattern matching:
double evaluate(Op op, ...) {
return switch (op) {
case Add a -> ...;
case Sub s -> ...;
// 8 more
};
}
Cost & Fix
For 10 cases with random distribution, the switch is a chain of `instanceof` (or hashed dispatch). Branch prediction works for skewed distributions; uniform distribution costs. **Fix:** 1. **Sort cases by frequency.** Most common first. 2. **Use a `MapOptimize 7 — Push Down Method causes downcast at callers (Java)¶
abstract class Employee {}
class Engineer extends Employee {
public double rate() { return 5000; } // pushed down
}
// Caller:
for (Employee e : list) {
if (e instanceof Engineer eng) total += eng.rate();
}
Cost & Fix
Each `instanceof` check + conditional add. ~1 ns per iteration. For most workloads: invisible. **Fix if hot:** 1. Iterate Engineer-typed lists separately. 2. Use polymorphism (Pull the method back up if all subclasses need it). Lesson: Push Down moves cost from one place to many. If callers proliferate `instanceof`, reconsider.Optimize 8 — Extract Superclass adds vtable level (Java)¶
Methods on Department previously dispatched via Department's vtable. Now via Party → Department, with one extra layer.
Cost & Fix
Vtable lookup is one indirection regardless of depth. **No additional cost.** The cost might come from: - Object header is the same size. - Methods inherited from Party are still dispatched virtually. - Field offsets may shift slightly. In practice: zero observable difference.Optimize 9 — Replace Delegation with Inheritance drops final (Java)¶
Original:
class Person {
private final Office office;
public String getAddress() { return office.getAddress(); }
}
Person.office is final — initialized once, never null.
"Refactored":
Now Person's fields are inherited; if Office's fields aren't final, they're mutable.
Cost & Fix
The replacement might introduce mutability where there was none. Caches and other immutability-dependent optimizations lose validity. **Fix:** Mark Office's fields `final`. Or, more often, **don't replace delegation with inheritance** — keep the delegate.Optimize 10 — Form Template Method allocates StringBuilder per call (Java)¶
public final String emit(Customer c) {
StringBuilder b = new StringBuilder();
b.append(header()); b.append(lines(c)); b.append(footer());
return b.toString();
}
For 1M emits: 1M StringBuilders, ~MB of garbage.
Cost & Fix
StringBuilder allocations are typical. For batch processing, the allocation rate is real. **Fix:** 1. Pass the StringBuilder in: `emit(Customer c, StringBuilder b)` — caller manages. 2. Pre-size: `new StringBuilder(estimateSize)`. 3. For huge batches: write directly to an output stream / writer. JIT typically optimizes short-lived StringBuilder allocations via escape analysis. Profile first.Optimize 11 — Inheritance hierarchy for serialization (Java + Jackson)¶
Without polymorphic type info:
String json = mapper.writeValueAsString(dog);
Animal a = mapper.readValue(json, Animal.class); // ❌ can't instantiate Animal
Cost & Fix
Deserialization fails. Or: with `@JsonTypeInfo`, every JSON adds a `"type": "Dog"` discriminator field. Each request adds a few bytes. **Fix options:** 1. Type info via property: `@JsonTypeInfo(use=Id.NAME, property="type")`. 2. Use sealed types + Jackson's pattern-match support (newer versions). 3. Avoid polymorphic JSON entirely: use separate endpoints / DTOs per concrete type. For high-throughput APIs, the per-message overhead matters.Optimize 12 — Mixin order changes performance (Python)¶
class Cache:
def get(self, k): ...
class Sync:
def get(self, k): ...
class A(Cache, Sync): pass # Cache.get takes precedence
class B(Sync, Cache): pass # Sync.get takes precedence
If Cache.get is fast and Sync.get is slow: - A is fast (Cache hit fast path). - B is slow (Sync always taken first).
Cost & Fix
MRO determines dispatch order. Mixin order affects performance. **Fix:** 1. Order mixins thoughtfully — fast paths first. 2. Document MRO assumptions. 3. Avoid deep mixin hierarchies for hot code; use composition. This is a subtle Python footgun. Inspect with `MyClass.__mro__`.Patterns¶
| Refactor | Cost |
|---|---|
| Pull Up Method | Virtual call instead of direct |
| Form Template Method | Multiple virtual calls per skeleton run |
| Replace Inheritance with Delegation | One extra deref (eliminated by JIT) |
| Pull Up Field carrying unused fields | Memory waste |
| Extract Interface in Go | Interface dispatch cost |
| Sealed types pattern matching | Linear case dispatch |
| Push Down forces caller instanceof | Per-iteration check |
| Extract Superclass | Negligible |
| Replace Delegation with Inheritance | Mutability concerns |
| StringBuilder per skeleton call | Allocation rate |
| Polymorphic JSON | Discriminator bytes |
| Mixin order | Dispatch chain length |