Card 06/ 09
Template Method Subclasses vs Passing Functions in Python
The fixed sequence becomes a function, and the varying steps become arguments.
A template method can be written without inheritance: the fixed sequence becomes a plain function, and each varying step becomes an argument that takes a function. This card writes the importer that way and sets out what each form gives you.
import csv
import io
import json
from collections.abc import Callable
Row = dict[str, str]
def run_import(
source: str,
parse: Callable[[str], list[Row]],
validate: Callable[[Row], bool] = lambda row: True,
) -> list[Row]:
rows = parse(source.strip())
valid = [row for row in rows if validate(row)]
print(f"{len(rows)} read, {len(valid)} stored")
return valid
def parse_csv(text: str) -> list[Row]:
return list(csv.DictReader(io.StringIO(text)))
run_import("name,email\nSam,[email protected]\nAli,[email protected]", parse_csv)
run_import(
'[{"name": "Kim", "email": "[email protected]"}, {"name": "Lee", "email": ""}]',
json.loads,
validate=lambda row: bool(row.get("email")),
)2 read, 2 stored
2 read, 1 storedThe results match the class version: two rows stored from CSV, one from JSON. There are no importer classes. run_import fixes the order, parse is a required argument, and validate has a default, which plays the hook's part. The JSON parser is the standard library's json.loads, passed in as it is.
This is composition in place of inheritance, and it turns each step into a small strategy. The order is now impossible to override, because there is no method to override.
| Concern | Subclasses | Functions as arguments |
|---|---|---|
| Protecting the order | Only by final in Java, or by convention | Nothing to override |
| Choosing the steps | When the class is written | When the function is called, and they can differ per call |
| Steps that share state | Fields on self, available to every step | Passed as arguments, or held in a closure |
| Many steps | Many methods, each with a name | A long argument list that is easy to get wrong |
Pass functions when there are one or two varying steps and they share no state, as in the importer. Use subclasses when there are many steps that share state through self, or when a framework must discover your code by its method names, as unittest finds every test_ method.
← → move through sections and cards