Card 07/ 09
A Subclass That Overrides the Template Method Itself
The subclass replaces the whole sequence, skips validation, and nothing stops it in Python.
The template method is the one part of the pattern a subclass must not replace, because it is what guarantees the order of the steps. A subclass that overrides it can drop a step, and in Python and TypeScript nothing stops it when the program runs. This card shows a subclass skipping validation, how Java refuses the same thing, and how to guard against it in Python.
A developer wants a faster JSON import. Instead of writing parse alone, they override run to parse and store in one line, and the base class's run is marked @final.
class FastJsonImporter(Importer):
def run(self, source: str) -> None:
self.stored.extend(self.parse(source))The input holds Kim, with an email, and Lee, without one, and the base class's validate rejects rows with no email.
Which rows are stored? Decide, then look.
import json
from abc import ABC, abstractmethod
from typing import final
Row = dict[str, str]
class Importer(ABC):
def __init__(self) -> None:
self.stored: list[Row] = []
@final
def run(self, source: str) -> None:
rows = self.parse(source)
self.stored.extend(row for row in rows if self.validate(row))
@abstractmethod
def parse(self, text: str) -> list[Row]: ...
def validate(self, row: Row) -> bool:
return bool(row.get("email"))
class FastJsonImporter(Importer):
def parse(self, text: str) -> list[Row]:
return json.loads(text)
def run(self, source: str) -> None:
self.stored.extend(self.parse(source))
importer = FastJsonImporter()
importer.run('[{"name": "Kim", "email": "[email protected]"}, {"name": "Lee", "email": ""}]')
print("stored:", [row["name"] for row in importer.stored])stored: ['Kim', 'Lee']Both rows are stored. FastJsonImporter.run replaced the template method, so validate was never called, and @final changed nothing at run time.
The cost is a rule that every import was meant to follow, broken by one subclass, with no error. The same code in Java does not compile.
public class TmFinal {
public static void main(String[] args) {
new Sneaky().templateMethod();
}
}
abstract class Base {
public final void templateMethod() {
System.out.println("base: the fixed order");
}
}
class Sneaky extends Base {
public void templateMethod() {
System.out.println("sneaky: my own order");
}
}java/TmFinal.java:14: error: templateMethod() in Sneaky cannot override templateMethod() in Base
public void templateMethod() {
^
overridden method is final
1 error
error: compilation failed
EXIT 1In Python the guard is a type checker. typing.final, defined in PEP 591, tells a checker such as mypy to report any subclass that overrides the method, so running one in continuous integration turns the mistake into a failed build. Without one, name the template method clearly, say in the class's documentation which methods subclasses should write, and test that every importer validates.
← → move through sections and cards