Template Method — Find the Bug¶
Each section presents a Template Method that looks fine but is broken. Find the bug yourself, then check.
Table of Contents¶
- Bug 1: Subclass overrides the template itself
- Bug 2: Public abstract steps callable out of order
- Bug 3: Subclass forgets to call super
- Bug 4: Constructor leaks
thisto template - Bug 5: Default hook with side effects
- Bug 6: Liskov substitution violation
- Bug 7: Asynchronous step blocks the template
- Bug 8: Recursive template re-entry
- Bug 9: Hook proliferation hides logic
- Bug 10: Concurrent invocation corrupts shared state
- Bug 11: Subclass throws unexpected exception
- Bug 12: Missing exhaustiveness in pattern match
- Practice Tips
Bug 1: Subclass overrides the template itself¶
public class Beverage {
public void make() { // not final
boilWater();
brew();
pourIntoCup();
}
protected void brew() {}
}
public class WeirdTea extends Beverage {
@Override
public void make() {
// skips boil water and pour
brew();
}
}
WeirdTea defeats the pattern.
Reveal
**Bug:** `make()` not `final`. Subclass overrides the entire template, breaking the algorithm structure. **Fix:** mark `make()` `final`. Subclasses can no longer override. **Lesson:** Mark Template Methods `final`. Lock the algorithm structure.Bug 2: Public abstract steps callable out of order¶
public abstract class Beverage {
public final void make() {
boilWater();
brew();
}
public abstract void brew(); // public
}
// Caller:
Tea tea = new Tea();
tea.brew(); // BUG: skipped boilWater
Reveal
**Bug:** `brew()` is public; callers can invoke it directly, bypassing the template. **Fix:** mark steps `protected`. Only subclasses (and `make()`) can invoke `brew()`. External callers must use `make()`. **Lesson:** Steps should be `protected`, not `public`. Encapsulate the lifecycle.Bug 3: Subclass forgets to call super¶
public abstract class Component {
protected void init() {
registerListeners();
loadConfig();
}
}
public final class MyComponent extends Component {
@Override
protected void init() {
// forgets super.init()
customSetup();
}
}
registerListeners and loadConfig never run.
Reveal
**Bug:** Subclass overrides `init()` but doesn't call `super.init()`. Parent's setup skipped. **Fix:** restructure so the base class controls the flow: Base class always runs its setup. Subclass adds via `customInit`. **Lesson:** Don't rely on subclasses calling `super`. Make the template `final` and provide explicit hooks.Bug 4: Constructor leaks this to template¶
public abstract class Processor {
protected final List<Hook> hooks;
public Processor() {
this.hooks = createHooks(); // calls overrideable method during construction
}
protected List<Hook> createHooks() { return new ArrayList<>(); }
}
public final class MyProcessor extends Processor {
private String config;
public MyProcessor(String config) {
super(); // calls createHooks() before this.config is set
this.config = config;
}
@Override
protected List<Hook> createHooks() {
return List.of(new ConfigHook(config)); // BUG: config is null
}
}
NPE in ConfigHook(config) because it's called before MyProcessor's constructor sets config.
Reveal
**Bug:** Constructor calls `createHooks()`, which is overridden. Subclass override runs before subclass constructor finishes initializing fields. **Fix:** don't call overridable methods from constructors. Use a separate `init()` method. Or use lazy initialization. **Lesson:** Constructors shouldn't call overrideable methods. Subclass fields not yet initialized.Bug 5: Default hook with side effects¶
public abstract class Pipeline {
public final void run() {
beforeRun();
process();
afterRun();
}
protected void beforeRun() {
log.info("starting");
metrics.startTimer(); // side effect
}
protected abstract void process();
protected void afterRun() {
metrics.stopTimer(); // expects startTimer
}
}
public final class MyPipeline extends Pipeline {
@Override
protected void beforeRun() {
// overrides to add custom logging; forgets to start timer
log.info("custom start");
}
@Override
protected void process() { /* ... */ }
}
afterRun calls stopTimer without startTimer. Metric corrupted.
Reveal
**Bug:** Default hook has side effects subclass might depend on. Override removes them. **Fix:** make critical setup `final`, expose user customization separately.public abstract class Pipeline {
public final void run() {
baseSetup();
userBeforeRun();
process();
userAfterRun();
baseTeardown();
}
private void baseSetup() {
metrics.startTimer();
}
private void baseTeardown() {
metrics.stopTimer();
}
protected abstract void process();
protected void userBeforeRun() {}
protected void userAfterRun() {}
}
Bug 6: Liskov substitution violation¶
public abstract class JsonParser {
public final Result parse(String json) {
validate(json); // base: ensures non-empty
return doParse(json);
}
protected void validate(String json) {
if (json.isEmpty()) throw new IllegalArgumentException("empty");
}
protected abstract Result doParse(String json);
}
public final class StrictJsonParser extends JsonParser {
@Override
protected void validate(String json) {
// override to add stricter checks
super.validate(json);
if (!json.startsWith("{")) throw new IllegalArgumentException("must start with {");
}
@Override
protected Result doParse(String json) { /* ... */ }
}
// Caller:
JsonParser p = new StrictJsonParser();
p.parse("[]"); // throws — but JsonParser.parse contract says any non-empty JSON works
Reveal
**Bug:** Subclass strengthens preconditions. Code expecting any `JsonParser` to accept any non-empty JSON breaks when given a `StrictJsonParser`. **Fix:** subclasses should weaken or maintain preconditions, not strengthen them. Or make the strict variant a separate type that doesn't claim to be a `JsonParser`. **Lesson:** Liskov substitution. Subclass methods must accept all inputs the base accepts. Strengthening preconditions is a violation.Bug 7: Asynchronous step blocks the template¶
public abstract class AsyncPipeline {
public CompletableFuture<Response> run(Request req) {
return validate(req)
.thenApply(this::handle) // BUG: handle returns Future
.thenApply(this::wrap);
}
protected abstract Response handle(Request req); // returns synchronous Response
protected CompletableFuture<Request> validate(Request req) { /* ... */ }
protected Response wrap(Response r) { /* ... */ }
}
public final class MyHandler extends AsyncPipeline {
@Override
protected Response handle(Request req) {
return restClient.callAsync(req).join(); // BLOCKS the executor thread
}
}
Reactor thread starvation.
Reveal
**Bug:** Template forces synchronous `handle`. Subclass that needs async work blocks. Reactor thread unavailable for other tasks. **Fix:** make `handle` return `CompletableFuture`.public abstract class AsyncPipeline {
public CompletableFuture<Response> run(Request req) {
return validate(req)
.thenCompose(this::handle) // thenCompose for async
.thenApply(this::wrap);
}
protected abstract CompletableFuture<Response> handle(Request req);
}
public final class MyHandler extends AsyncPipeline {
@Override
protected CompletableFuture<Response> handle(Request req) {
return restClient.callAsync(req);
}
}
Bug 8: Recursive template re-entry¶
public abstract class Job {
public final void run() {
beforeRun();
process();
afterRun();
}
protected void afterRun() {} // hook
protected abstract void process();
}
public final class RetryingJob extends Job {
@Override
protected void afterRun() {
if (failed()) run(); // BUG: re-enters run from within run
}
}
Stack overflow on retry.
Reveal
**Bug:** `afterRun` calls `run()` directly. Re-entry into template; stack grows. **Fix:** schedule the retry, don't call recursively. Or move retry logic outside the template. **Lesson:** Template hooks shouldn't re-enter the template. Schedule or restructure.Bug 9: Hook proliferation hides logic¶
public abstract class Workflow {
public final void run() {
beforeStart();
if (shouldStart()) {
beforeAcquireResources();
acquireResources();
afterAcquireResources();
beforeProcess();
if (shouldProcess()) {
process();
}
afterProcess();
beforeReleaseResources();
releaseResources();
afterReleaseResources();
}
afterEnd();
}
// 12 hooks, all defaulting to no-op
}
What does the workflow do? Trace by reading subclasses one by one.
Reveal
**Bug:** Hook proliferation. The template is configuration; logic scattered across overrides. Discoverability tanked. **Fix:** consolidate. Either fewer hooks, or use Strategy / middleware: Composition over inheritance for highly customizable lifecycles. **Lesson:** Hook proliferation is a smell. When you have 10+ hooks, switch to Strategy / middleware.Bug 10: Concurrent invocation corrupts shared state¶
public abstract class StatefulPipeline {
private List<String> buffer = new ArrayList<>(); // base-class field
public final void run(String input) {
buffer.clear();
process(input);
save(buffer);
}
protected final void emit(String item) {
buffer.add(item);
}
protected abstract void process(String input);
private void save(List<String> data) { /* ... */ }
}
// Two threads call run concurrently with same instance:
pipeline.run("a");
pipeline.run("b"); // races on buffer
Reveal
**Bug:** Shared mutable state in the base class. Concurrent calls race. **Fix:** make the pipeline stateless; pass state explicitly. Or synchronize: Synchronization serializes; defeats throughput. **Lesson:** Stateful templates are concurrency hazards. Prefer stateless; pass state through call parameters.Bug 11: Subclass throws unexpected exception¶
public abstract class FileLoader {
public final List<Row> load(String path) {
String content = read(path);
return parse(content);
}
protected String read(String path) throws IOException { /* ... */ }
protected abstract List<Row> parse(String content);
}
public final class StrictParser extends FileLoader {
@Override
protected List<Row> parse(String content) {
if (content.isBlank()) throw new IllegalStateException("empty file");
// ...
}
}
// Caller:
try {
parser.load(path);
} catch (IOException e) { /* expected */ }
// IllegalStateException unhandled — crashes app
Reveal
**Bug:** Subclass throws exception type not declared by the template. Callers can't handle it generically. **Fix:** subclasses should throw exceptions consistent with the contract. Or wrap: Document the exception contract. **Lesson:** Exception types are part of the contract. Subclasses must respect what callers can catch.Bug 12: Missing exhaustiveness in pattern match¶
sealed interface State permits Draft, Moderation, Published {}
public final class Workflow {
public String describe(State state) {
return switch (state) {
case Draft d -> "draft";
case Moderation m -> "moderating";
// BUG: missing Published case
default -> "unknown";
};
}
}
Adding Archived to the sealed interface compiles fine; default branch hides the issue.
Reveal
**Bug:** `default` clause swallows all unhandled cases. New states added to the sealed interface fall through silently. Defeats compile-time exhaustiveness. **Fix:** remove `default`; let the compiler force all cases handled. Adding `Archived` now forces compile error here. Refactoring is safe. **Lesson:** Don't use `default` with sealed types unless intentional. Compile-time exhaustiveness is the whole point.Practice Tips¶
- Mark Template Methods
final. Lock structure. - Mark steps
protected. Encapsulate lifecycle. - Don't rely on subclasses calling
super. Restructure withfinaltemplate + hooks. - Don't call overridable methods from constructors. Subclass fields not yet initialized.
- Critical lifecycle in
finalmethods. Hooks for user customization. - Liskov: subclasses can't strengthen preconditions.
- Async templates use Future / Promise types end-to-end.
- Don't re-enter the template from within hooks.
- Hook proliferation = refactor. Strategy or middleware.
- Stateful templates are concurrency hazards. Pass state through calls.
- Subclasses must respect exception contracts.
- Avoid
defaultin pattern matches over sealed types.