Patterns you will actually be asked for · chapter 25 of 33
Template method: a fixed skeleton with holes
Chapter 3.7 · Part 3, Patterns you will actually be asked for · about 25 minutes
What you need before this chapter: Part 1 in full, especially abstract classes (1.5). Part 2 in full, especially open/closed (2.5) and Liskov substitution (2.6). Chapter 3.1, Strategy, for comparison: that chapter swaps one decision, and this one owns a sequence of several.
When you finish this chapter you will be able to:
- Recognise when a fixed sequence of steps, repeated for several variants, is at risk of being written slightly differently each time
- Write an abstract class that seals the sequence with
finaland exposes hook methods for the parts that actually differ - Choose between an abstract class and a
defaultinterface method for the same idea, based on whether the sequence needs to carry state - Say when one fixed order with no real variant is not worth this shape at all
1. The situation
An order moves through four stages: placed, accepted, preparing, ready. Moving to any stage does the same three things: check the move is legal, apply it, and record what happened for the customer. The obvious way to write this is one method that does all three for every stage.
static void advance(Order order, OrderState next) {
if (order.state.ordinal() + 1 != next.ordinal()) {
throw new IllegalStateException("cannot move from " + order.state + " to " + next);
}
order.state = next;
order.history.add("moved to " + next + ", notified customer");
}
2. Naive code that is fine
javac Step1.java
java Step1
moved to ACCEPTED, notified customer
moved to PREPARING, notified customer
moved to READY, notified customer
For four stages that all follow the exact same three steps, this is easy to read, and a reviewer can check it against the requirement without any trouble.
3. A new requirement
Cancellation arrives, and it does not fit the pattern the other four stages share. It is only legal while an order is still PLACED or ACCEPTED, and it also has to zero the charge, which no other stage does. The obvious response is a special case, checked first.
static void advance(Order order, OrderState next) {
if (next == OrderState.CANCELLED) {
if (order.state != OrderState.PLACED && order.state != OrderState.ACCEPTED) {
throw new IllegalStateException("too late to cancel from " + order.state);
}
order.state = OrderState.CANCELLED;
order.chargeCents = 0;
order.history.add("cancelled, refunded, notified customer");
return;
}
if (order.state.ordinal() + 1 != next.ordinal()) {
throw new IllegalStateException("cannot move from " + order.state + " to " + next);
}
order.state = next;
order.history.add("moved to " + next + ", notified customer");
}
javac Step2.java
java Step2
charge: 0
moved to ACCEPTED, notified customer
cancelled, refunded, notified customer
This works, and it is still a method a reviewer can read top to bottom. It is worth noticing exactly what happened, though: the fixed idea of "check, apply, record" got repeated by hand a second time, with its own separate legality rule and its own separate recording line, right next to the first copy.
4. Watch where it goes, and the real cost
A sixth stage will need the same three steps written out a third time. Whoever adds it has to remember, from scratch, to check legality first, apply the change second, and record it third, in that order, inside a method that is growing a new special case for every stage that needs one. The three steps are not hard to write once. They are easy to get subtly wrong the third time, under deadline pressure, in the middle of a method that already has two other variants tangled through it. A stage that records itself before actually checking whether the move was legal would compile and would look almost right.
5. The move
Fix the three-step sequence once, in a method nothing can override, and let each stage plug in only the two things that actually differ: how it checks legality, and what extra happens once it has been entered.
abstract class OrderStage {
private final OrderState state;
OrderStage(OrderState state) {
this.state = state;
}
// The fixed sequence. No subclass can reorder, skip, or wrap these three steps.
final void enter(Order order) {
check(order);
order.state = state;
String note = onEntered(order);
order.history.add("moved to " + state + (note.isEmpty() ? "" : ", " + note) + ", notified customer");
}
void check(Order order) { }
String onEntered(Order order) { return ""; }
}
A stage with nothing special to do overrides neither hook.
final class PlainStage extends OrderStage {
PlainStage(OrderState state) { super(state); }
}
CancelledStage overrides both.
final class CancelledStage extends OrderStage {
CancelledStage() { super(OrderState.CANCELLED); }
@Override
void check(Order order) {
if (order.state != OrderState.PLACED && order.state != OrderState.ACCEPTED) {
throw new IllegalStateException("too late to cancel from " + order.state);
}
}
@Override
String onEntered(Order order) {
order.chargeCents = 0;
return "refunded";
}
}
javac Step3.java
java Step3
charge: 0
moved to ACCEPTED, notified customer
moved to CANCELLED, refunded, notified customer
enter is final, so check, then the move, then onEntered, then the recording line, happen in that order, every time, for every stage that will ever exist. PlainStage needed no new class at all, just a constructor call, because a stage with nothing special about it is data, not behaviour. This is Template Method: one method owns the order steps run in, and each stage only fills in the two blanks that are actually its own. corpus/food-ordering's own OrderStage and CancelledStage are this same shape, five fixed steps and the same two hooks, on a six-stage order lifecycle.
6. What modern Java changes here
An interface with a default method can carry a fixed sequence too, and it is worth reaching for when a class already extends something else and has no spare slot in its hierarchy for OrderStage. The tie-breaker between the two is not style. It is whether the sequence needs to carry state from one step to the next. An interface cannot answer yes to that: it can declare methods, but it cannot declare an instance field the way OrderStage could if a later stage needed one.
The other real difference is that a default method cannot be declared final. Nothing stops an implementer from overriding the whole sequence by accident, skipping every hook it was supposed to call through.
interface ApprovalWorkflow {
default boolean approve(Request request) {
if (!checkFormat(request)) return false;
if (!checkLimit(request)) return false;
return true;
}
boolean checkFormat(Request request);
boolean checkLimit(Request request);
}
static final class SkipsChecks implements ApprovalWorkflow {
@Override
public boolean approve(Request request) { return true; }
@Override
public boolean checkFormat(Request request) { return false; }
@Override
public boolean checkLimit(Request request) { return false; }
}
javac Step5.java
java Step5
standard.approve(tooBig): false
skips.approve(tooBig): true
SkipsChecks compiles cleanly and approves a request that both of its own hooks would reject, because overriding approve directly is completely legal for a default method. ApprovalWorkflow needs no field here, since both hooks only ever look at the Request they are handed, so an interface is the right shape. OrderStage's sequence, by contrast, was worth sealing with final specifically because skipping the legality check was the exact mistake section 4 was trying to prevent.
7. When naming it is wrong
A single order lifecycle with no second variant ever planned does not need enter split into hooks. Writing the three steps inline once, the way section 2 does, costs nothing extra, and it stays easier to read end to end when there is only one path through it.
The threshold: reach for this once at least two variants genuinely share one fixed sequence, and only a couple of steps differ between them. One order type, with CANCELLED never on the roadmap, does not meet that bar, and building the hook machinery for it scores as `over-engineered (premature interface)` under this app's grading standard, the same tag every earlier chapter's speculative interface has earned.
Your turn
Add a READY stage that also sends an SMS to the customer, using onEntered, with no change to OrderStage.
The answer.
final class ReadyForPickupStage extends OrderStage {
ReadyForPickupStage() { super(OrderState.READY); }
@Override
String onEntered(Order order) {
return "SMS sent";
}
}
javac Step4.java
java Step4
moved to ACCEPTED, notified customer
moved to PREPARING, notified customer
moved to READY, SMS sent, notified customer
One new class, one overridden hook. OrderStage.enter ran the same three steps it always does.
Going deeper
Try the same trick SkipsChecks used against OrderStage itself, where enter is final.
static final class BadStage extends OrderStage {
BadStage() { super(OrderState.ACCEPTED); }
@Override
void enter(Order order) {
// skip the check entirely
}
}
javac Step6Bad.java
Step6Bad.java:27: error: enter(Order) in BadStage cannot override enter(Order) in OrderStage
void enter(Order order) {
^
overridden method is final
1 error
This is the whole reason to prefer an abstract class over a default method whenever the choice is open. final on a method is only legal in a class, never on an interface method, default or otherwise. When the fixed sequence is protecting an invariant that actually matters, the way "check before you charge" does for OrderStage, that difference is not a style preference. It is the difference between a mistake the compiler catches and a mistake that ships.
Why this matters in an interview
An interviewer who asks you to add a fifth or sixth variant to a working design is watching for one thing. Do you edit the same lines a third time, or do you notice that three steps keep repeating and give them one home? Naming Template Method matters less than being able to say which two things actually vary between your stages, and being able to explain why you sealed the rest with final instead of leaving it open to a mistake like BadStage's.
Next: chapter 3.8, Adapter and Facade: making other people's code fit. This chapter fixed the order steps run in in your own code. The next one deals with code you do not own, and cannot change, that does not fit the shape your code needs.
← 3.6 Decorator and Chain of Responsibility · All chapters · 3.8 Adapter and Facade: making other people's code fit →