application framework 通过复用已有 class、并 override method 来定制 behavior,从而构建新的 application。 framework 的核心是 GoF Design Patterns 中的 Template Method: 它是定义在 base class 中的一个 method, 通过调用其他 base-class method 来驱动 application, 其中一些 method 由你 override。

Python 自己的 unittest 就是这种 application framework。 你 subclass TestCase,并提供 setUp()、自己的 test_* method, 以及 tearDown()。 TestCase.run() 就是 template method。 它先调用 setUp(),然后调用你的 test method,最后调用 tearDown()。 仅仅构造一个 TestCase 不会运行任何东西。 test runner 会在完整 object 上调用 run()。

被锚定的 Algorithm

Template Method 会把 algorithm 的整体形状锚定在 base class 中。 subclass 负责提供各个具体 step。 typing.final decorator, 在 Making a Class Final 中用于 class, 同样也能用于单独的 method。 它会锁住 template method,使 subclass 不能改变 flow。 这里,给 run() 加上 @final 后,任何 override 它的 subclass 都会被拒绝:

# template_method.py
from typing import final, override

class ApplicationFramework:
    @final
    def run(self) -> None:
        for _ in range(2):
            self.customize1()
            self.customize2()

    def customize1(self) -> None: ...
    def customize2(self) -> None: ...

# Create an application by filling in the steps:
class MyApp(ApplicationFramework):
    @override
    def customize1(self) -> None:
        print("Nudge, nudge, wink, wink!")

    @override
    def customize2(self) -> None:
        print("Say no more, say no more!")

MyApp().run()
#: Nudge, nudge, wink, wink!
#: Say no more, say no more!
#: Nudge, nudge, wink, wink!
#: Say no more, say no more!

client 在 derived class 中提供 customize1() 和 customize2()。 run() 启动驱动整个 application 的 engine。

base class 会调用以后才写出来的代码,有时甚至是几年以后才写。 framework 作者把这称为 Hollywood Principle:“don’t call us, we’ll call you.” 这种反转的一般名称是 Inversion of Control: framework 定义 control flow,并调用你的代码, 而不是由你的代码去调用一个 library。

只有 type checker 会强制执行 @final。 runtime 中,这个 decorator 只是在 function 上设置 __final__ = True, interpreter 里没有任何东西读取这个 attribute。 如果希望 interpreter 自己拒绝 override, Making a Class Final 里的 __init_subclass__() technique 同样可以用于 method: 在发现 "run" in cls.__dict__ 时抛异常。

这些 step method 默认都是 ..., 因此 subclass 只需要 override 它关心的 step, 漏掉的 step 会悄无声息地什么也不做。 这种 optional step 称为 hook。 开头例子里的 setUp() 和 tearDown() 就是 hook: TestCase 提供 do-nothing version, 所以不需要 setup 的 test class 可以直接跳过它们。 这种 silent behavior 会掩盖拼写错误:def customise1()(s 而不是 z) 会添加一个新 method,同时 base 里的 do-nothing version 仍然保留。 这就是为什么这些 listing 中每个 step override 都带有 @override: 这样 type checker 会拒绝一个实际上没有 override 任何东西的 method。

checker 看到的只有 decorator。 如果 misspelled method 没写 @override,checker 会把它当作新 method 并接受。 没有任何 typing construct 能禁止 subclass 添加 method, 所以 checker 捕捉不到这种情况。 interpreter 则可以。 base class 的 __init_subclass__() 会在每个 subclass 的 class statement 执行, standard library 的 difflib 则用来找出近似匹配的 name:

# near_miss.py
from difflib import get_close_matches
from typing import final, override

class ApplicationFramework:
    @final
    def run(self) -> None:
        for _ in range(2):
            self.customize1()
            self.customize2()

    def customize1(self) -> None: ...
    def customize2(self) -> None: ...

    def __init_subclass__(cls) -> None:
        super().__init_subclass__()
        hooks = {
            name
            for base in cls.__mro__[1:]
            for name in vars(base)
            if not name.startswith("__")
        }
        for name in vars(cls):
            if name in hooks or name.startswith("__"):
                continue
            if near := get_close_matches(name, hooks):
                raise TypeError(
                    f"{cls.__name__}.{name}: "
                    f"did you mean{near[0]}?"
                )

class MyApp(ApplicationFramework):
    @override
    def customize1(self) -> None:
        print("one")

    def report(self) -> None: ...

try:
    class Typo(ApplicationFramework):
        def customise1(self) -> None:
            print("never runs")
except TypeError as e:
    print(e)
#: Typo.customise1: did you mean customize1?

hooks 收集 base class 定义的每个 non-dunder name。 如果 subclass 的 name 与其中某个完全相同,那就是 override; 如果它与任何 name 都不相似,例如 report(), 那就是普通的新 method。 这两种情况都会通过。 只有 near miss 才会产生 TypeError, 而 error message 会指出作者很可能本来想写的 method。 class Typo statement 会直接抛 TypeError,无法完成定义, 所以拼写错误会在 import time 就失败, 而不是等到 framework 真正运行后,某个 step 才静悄悄地什么也不做。 当然,禁止所有新 method 也能抓住 typo, 但那样也会禁止 report(); 一个不允许 subclass 添加 helper method 的 framework 限制太强。

如果每个 subclass 都必须提供某个 step, 可以继承 ABC,并给那个 step 加 @abstractmethod, 如 Rethinking Objects 所示。 这样 runtime 会拒绝实例化忘记实现它的 subclass。

下面的 test 提供一个 recording subclass,并验证 anchored flow:

# test_template_method.py
from typing import override
from template_method import ApplicationFramework

def test_template_method_runs_steps_in_order() -> None:
    calls: list[str] = []

    class Recorder(ApplicationFramework):
        @override
        def customize1(self) -> None:
            calls.append("one")

        @override
        def customize2(self) -> None:
            calls.append("two")

    Recorder().run()  # The client starts the engine
    # Loop runs twice
    assert calls == ["one", "two", "one", "two"]

不要在 Constructor 里启动 Engine

启动 engine 的应该是 client,而不是 ApplicationFramework。 framework 当然可以 在自己的 constructor 中调用 run(), 但这样一来,定义了自己 __init__() 的 subclass 就会掉进陷阱。 因为 run() 会调用 subclass 提供的 method, subclass 必须先完成自己的 setup,才能调用 super().__init__()。 如果按 Python programmer 通常习惯的写法先调用 super().__init__(), engine 就会运行在一个只初始化到一半的 object 上:

# premature_engine.py
from typing import final, override

class Framework:
    def __init__(self) -> None:
        self.run()

    @final
    def run(self) -> None:
        self.step()

    def step(self) -> None: ...

class Greeter(Framework):
    def __init__(self, name: str) -> None:
        # With the usual style, the engine calls run()
        super().__init__()
        self.name = name  # ...before this line runs

    @override
    def step(self) -> None:
        print(f"Hello,{self.name}!")

try:
    Greeter("Robin")
except AttributeError as e:
    print(e)
#: 'Greeter' object has no attribute 'name'