Template Method and Iterator
In short: Template Method keeps the shared skeleton of an algorithm in a base class while subclasses fill in the steps; Iterator hands out elements one by one without showing how they are stored.
Problem
Two reports with the same skeleton
Accounting wants the orders in CSV, and the website wants the same ones in an HTML table. Cancelled orders don't go into the report. Someone wrote the CSV one, and then copied the function for HTML:
function exportCsv(orders) { const lines = ["order,total"]; let total = 0; for (const order of orders) { if (order.cancelled) { continue; } lines.push(`${order.id},${order.total.toFixed(2)}`); total += order.total; } lines.push(`total,${total.toFixed(2)}`); return lines.join("\n");}
function exportHtml(orders) { const lines = ["<table>", "<tr><th>Order</th><th>Total</th></tr>"]; let total = 0; for (const order of orders) { lines.push(`<tr><td>${order.id}</td><td>${order.total.toFixed(2)}</td></tr>`); total += order.total; } lines.push(`<tr><td>Total</td><td>${total.toFixed(2)}</td></tr>`); lines.push("</table>"); return lines.join("\n");}const orders = [ { id: 1001, total: 80, cancelled: false }, { id: 1002, total: 25.5, cancelled: false }, { id: 1003, total: 40, cancelled: true },];console.log(exportCsv(orders));console.log(exportHtml(orders));order,total1001,80.001002,25.50total,105.50<table><tr><th>Order</th><th>Total</th></tr><tr><td>1001</td><td>80.00</td></tr><tr><td>1002</td><td>25.50</td></tr><tr><td>1003</td><td>40.00</td></tr><tr><td>Total</td><td>145.50</td></tr></table>The two reports show a different total: the CSV says €105.50, and the HTML says €145.50. The rule "cancelled orders don't count" exists only in the first copy. Both copies do the same thing: they open, write a header, go over the orders, write the total and close. Only the lines they write are different. A third format will be a third copy, and the rule will have to be remembered in one more place.
The idea
Template Method: one skeleton, different steps
What doesn't change is the order of the steps and the rules between them: open, header, a row for each order except the cancelled ones, total, close. What changes is what each step looks like.
The skeleton goes into the base class Report, in the method export. At every place where the formats differ, it calls a step: header, row, footer. The subclass writes only the steps. A step that is optional (open and close, which CSV doesn't use) has an empty body in the base class and is called a hook.
This is the Template Method pattern. It has two roles:
- the base class with the skeleton: it decides the order and calls the steps;
- the subclasses:
CsvReportandHtmlReportwrite the steps and call nothing themselves.
See it
The skeleton and the two reports
abstract. A required step in the base class throws an error if the subclass hasn't written it. The two hooks are empty:class Report { export(orders) { const lines = []; this.open(lines); this.header(lines); let total = 0; for (const order of orders) { if (order.cancelled) { continue; } this.row(lines, order); total += order.total; } this.footer(lines, total); this.close(lines); return lines.join("\n"); }
open(lines) {}
header(lines) { throw new Error("header() must be written"); }
row(lines, order) { throw new Error("row() must be written"); }
footer(lines, total) { throw new Error("footer() must be written"); }
close(lines) {}}The two reports write only their lines. CsvReport doesn't write the hooks, so it uses the empty ones from the base class:
class CsvReport extends Report { header(lines) { lines.push("order,total"); }
row(lines, order) { lines.push(`${order.id},${order.total.toFixed(2)}`); }
footer(lines, total) { lines.push(`total,${total.toFixed(2)}`); }}
class HtmlReport extends Report { open(lines) { lines.push("<table>"); }
header(lines) { lines.push("<tr><th>Order</th><th>Total</th></tr>"); }
row(lines, order) { lines.push(`<tr><td>${order.id}</td><td>${order.total.toFixed(2)}</td></tr>`); }
footer(lines, total) { lines.push(`<tr><td>Total</td><td>${total.toFixed(2)}</td></tr>`); }
close(lines) { lines.push("</table>"); }}const orders = [ { id: 1001, total: 80, cancelled: false }, { id: 1002, total: 25.5, cancelled: false }, { id: 1003, total: 40, cancelled: true },];console.log(new CsvReport().export(orders));console.log(new HtmlReport().export(orders));order,total1001,80.001002,25.50total,105.50<table><tr><th>Order</th><th>Total</th></tr><tr><td>1001</td><td>80.00</td></tr><tr><td>1002</td><td>25.50</td></tr><tr><td>Total</td><td>105.50</td></tr></table>class Report { export(orders) { const lines = []; this.open(lines); this.header(lines); let total = 0; for (const order of orders) { if (order.cancelled) { continue; } this.row(lines, order); total += order.total; } this.footer(lines, total); this.close(lines); return lines.join("\n"); }
open(lines) {}
header(lines) { throw new Error("header() must be written"); }
row(lines, order) { throw new Error("row() must be written"); }
footer(lines, total) { throw new Error("footer() must be written"); }
close(lines) {}}Step by step, in words
- new CsvReport().export(orders): the skeleton is in Report. It decides the order of the steps.
- open: CsvReport doesn't write this step, so the empty body from Report runs. Nothing is added.
- header: the skeleton calls the step, and CsvReport writes its line.
- row for order 1001: the skeleton calls the step, and CsvReport writes its line.
- row for order 1002: the skeleton calls the step, and CsvReport writes its line.
- Order 1003 is cancelled: the skeleton itself skips it with continue. No report has to remember this rule.
- footer: the skeleton calls the step, and CsvReport writes its line.
- close: CsvReport doesn't write this step, so the empty body from Report runs. Nothing is added.
- Now HtmlReport: the same skeleton, other steps.
- open: the skeleton calls the step, and HtmlReport writes its line.
- header: the skeleton calls the step, and HtmlReport writes its line.
- row for order 1001: the skeleton calls the step, and HtmlReport writes its line.
- row for order 1002: the skeleton calls the step, and HtmlReport writes its line.
- Order 1003 is cancelled: the skeleton itself skips it with continue. No report has to remember this rule.
- footer: the skeleton calls the step, and HtmlReport writes its line.
- close: the skeleton calls the step, and HtmlReport writes its line.
- Two reports, one skeleton. Both totals are €105.50, because the rule about cancelled orders is in one place.
The total is €105.50 in both reports: the rule about cancelled orders is in the skeleton, in one place.
See it
The skeleton is not touched
JavaScript has no final. A subclass can write its own export, and nothing stops it. A missing step shows up only when the skeleton reaches it:
class TextReport extends Report { header(lines) { lines.push("order total"); }
row(lines, order) { lines.push(`${order.id} ${order.total.toFixed(2)}`); }}
console.log(new TextReport().export(orders));Error: footer() must be writtenSo every new report is run at least once before it reaches accounting.
See it
Without inheritance: the steps as functions
The steps don't have to be methods of a subclass. The skeleton can take them as parameters: the header is text, and the row and the total are functions.
function exportOrders(orders, header, row, footer) { const lines = [header]; let total = 0; for (const order of orders) { if (order.cancelled) { continue; } lines.push(row(order)); total += order.total; } lines.push(footer(total)); return lines.join("\n");}console.log(exportOrders( orders, "order,total", (order) => `${order.id},${order.total.toFixed(2)}`, (total) => `total,${total.toFixed(2)}`,));console.log(exportOrders( orders, "<table>\n<tr><th>Order</th><th>Total</th></tr>", (order) => `<tr><td>${order.id}</td><td>${order.total.toFixed(2)}</td></tr>`, (total) => `<tr><td>Total</td><td>${total.toFixed(2)}</td></tr>\n</table>`,));order,total1001,80.001002,25.50total,105.50<table><tr><th>Order</th><th>Total</th></tr><tr><td>1001</td><td>80.00</td></tr><tr><td>1002</td><td>25.50</td></tr><tr><td>Total</td><td>105.50</td></tr></table>The skeleton is still in one place, and cancelled orders still don't count.
In JavaScript this is often the more natural way: a function is a value, and no inheritance is needed. A subclass pays off when there are many steps or they share data (for example the current row number), which sits better in the fields of one object.Problem
Everyone who wants the orders turns the pages
The orders are in a system that gives them in pages: two per request, because a request is slow. Here the source is a list in memory, and every load prints a line, so you can see when it happens:
class OrderSource { constructor(orders) { this.orders = orders; }
fetchPage(number) { console.log(`loading page ${number}`); const start = (number - 1) * 2; return this.orders.slice(start, start + 2); }}The report, the email to customers and the export all want every order, and each one turns the pages itself:
let number = 1;let page = source.fetchPage(number);while (page.length > 0) { for (const order of page) { console.log(order.id); } number++; page = source.fetchPage(number);}loading page 110011002loading page 210031004loading page 31005loading page 4The code that only wants "each order in turn" knows that there are pages, how they are counted, and that the end is an empty page. The same lines are in three files. When the system moves from page numbers to "a key for the next page", you change all three.
The idea
Iterator: going over things behind a cursor
The caller wants one thing: the next order, while there is one. How to get to it (a list, pages, a file, the network) is someone else's job.
That someone is the cursor: an object that remembers how far it got and answers the question "is there more?" by itself, loading a new page only when the previous one is over. The caller writes an ordinary for...of and doesn't know about the pages.
This is the Iterator pattern. It has two roles:
- the collection: the thing being gone over. It gives a cursor;
- the cursor: remembers the place and gives the next element.
This pattern is built into each of the four languages: for...of works with every object that gives a cursor. In JavaScript the cursor is an object with next(), and the easiest way to make one is a generator: a function with function* and yield.
See it
The cursor that loads the pages
function* makes a generator: calling it does nothing yet, it returns a cursor. Every time for...of asks for the next order, the function runs to the next yield, gives the order and pauses right there. That's why the next page is loaded only when the current one is over:function* allOrders(source) { for (let number = 1; ; number++) { const page = source.fetchPage(number); if (page.length === 0) { return; } for (const order of page) { yield order; } }}The caller doesn't know about the pages:
for (const order of allOrders(source)) { console.log(order.id);}loading page 110011002loading page 210031004loading page 31005loading page 4function* allOrders(source) { for (let number = 1; ; number++) { const page = source.fetchPage(number); if (page.length === 0) { return; } for (const order of page) { yield order; } }}
for (const order of allOrders(source)) { console.log(order.id);}Step by step, in words
- The loop asks for the first order. Nothing has been loaded yet.
- The generator has no page yet, so it loads page 1.
- 1001: the first order from the page goes to the loop, and the loop prints it.
- 1002: from the same page. Nothing is loaded.
- The page is over. Only now does the cursor load page 2.
- 1003 and 1004 from page 2.
- The border again: page 3.
- 1005: page 3 has only one order.
- Page 4 is empty: there are no more orders.
- The cursor says "no more", and the loop ends. The loop never saw a page.
The output is the same, and counting pages is in one place. When the system changes the way it gives pages, you change the generator, and the three places that go over the orders aren't touched.
See it
What for-each does inside
for...of is a short way to write this loop with the cursor:
const it = allOrders(source)[Symbol.iterator]();for (let step = it.next(); !step.done; step = it.next()) { const order = step.value; console.log(order.id);}loading page 110011002loading page 210031004loading page 31005loading page 4for...of calls [Symbol.iterator]() once, and then next() on every step, until done becomes true. A generator is its own cursor: [Symbol.iterator]() returns the generator itself.If you want something that can be gone over again every time, like a list, make a class that gives a new generator on every pass:
class AllOrders { constructor(source) { this.source = source; }
[Symbol.iterator]() { return allOrders(this.source); }}Now for (const order of new AllOrders(source)) starts from the beginning on every pass, just like an Iterable in Java.
Try it
Predict the output
The loop stops after the third order. What does the program print?
let shown = 0;for (const order of allOrders(source)) { console.log(order.id); shown++; if (shown === 3) { break; }}Try it
Find the bug
The email to customers first says how many orders there are, and then lists them:
const orders = allOrders(new OrderSource(all));let count = 0;for (const order of orders) { count++;}console.log(`Orders: ${count}`);for (const order of orders) { console.log(order.id);}loading page 1loading page 2loading page 3loading page 4Orders: 5Pick the line with the bug.
for...of uses it up to the end, and the second one has nothing to take: it prints nothing and gives no error. If you need the orders twice, collect them into an array once: const orders = [...allOrders(...)];. Or use the class AllOrders, which gives a new generator on every pass, if it's fine for the pages to be loaded again.The idea
When you don't need them
Template Method has a cost: a base class and inheritance, and inheritance is a tie that is hard to undo. It isn't worth it when:
- there is one format. One function without steps is clearer;
- there are one or two steps. Pass them as functions, as in the card "Without inheritance";
- the subclasses want to change the order of the steps too. Then the skeleton isn't shared, and forcing it gives you steps with an
ifinside.
You don't need your own Iterator when the data is already in a array: it can already be gone over. You write your own when there is something behind it that isn't a collection in memory: pages, a file read line by line, a tree, or an endless sequence (invoice numbers).
Exercise
A report in Markdown
Solve the task in your editor.
Markdown. The documentation site wants the report as a Markdown table. Write MarkdownReport, which writes only the steps. The header is two lines. Report and the other reports are not touched. With the orders from the lesson:
| Order | Total ||---|---|| 1001 | 80.00 || 1002 | 25.50 || **Total** | 105.50 |More tasks
- Row numbers. The HTML report numbers its rows (
<td>1</td>,<td>2</td>), and cancelled orders don't count. Where do you keep the counter: in the skeleton or inHtmlReport? - The first N. Write a generator that gives only the first N orders from another cursor. How many pages are loaded for the first three?
- Only the big ones. Go over the orders from the source and give only those over €30, again without the caller knowing about the pages.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Arrow functions · Data, switch and destructuring · Design patterns · Strategy
- Tasks on this topic: Design patterns