别再滥用设计模式:用函数对象和新语言特性做“降维替代”

当你在 Python、JavaScript、现代 Java 或 Go 中为一个"策略"写接口和三个实现类时,你很可能不是在运用设计智慧,而是在用代码模拟一种语言本该直接提供的能力。** 多数 GoF 模式诞生于语言表达力贫瘠的年代,它们的本质是"用对象结构弥补缺少一等函数和语法糖"。今天继续机械地套用这些模式,得到的不是可维护性,而是一层需要额外维护的翻译层。

模式滥用的真正代价:把语言缺陷当成设计智慧

"模式过敏症"(patternitis)的典型症状是:学过策略模式之后,连一个三行的 if-else 都要拆成 Strategy 接口、ConcreteStrategyA、ConcreteStrategyB 和 Context 四个文件。一个计算器的加减乘除被包装成四个策略类,新增开发人员需要先理解整套策略层级才能看懂"加法在哪里"。

这种写法不是没有"设计",而是设计的成本超过了它解决的问题。在互联网软件的快速迭代节奏下,这个问题被进一步放大:策略模式为了满足迪米特法则,只向策略传递"刚好需要"的参数;一旦计算逻辑需要多用一个字段,过程式代码可以直接从上下文中取用,而策略模式却需要在 Context 中新增字段、在入口处设置参数、再到策略实现中消费它------一个字段的变更穿透三层。开闭原则承诺的"对扩展开放"在这里变成了"对修改关闭"的诅咒:每一个微小的参数变化都被迫走完整个抽象链路。

更根本的问题在于,很多人把 GoF 模式当成了写代码时的"菜谱",而不是沟通时的"词汇"。模式真正的价值在于说"这里用了外观模式"时团队能瞬间对齐,而不在于对着 UML 图往代码里硬塞抽象接口。当语言已经能直接表达意图时,继续用模式"翻译"一遍,就是在给本来不存在的问题制造解决方案。

用函数对象取代"单方法接口"模式

Strategy、Command、Template Method 这三个模式有一个共同的解剖学特征:它们封装的核心都是一个"行为",而不是一个"事物"。在 Java 8 之前,行为不能独立存在,必须寄生在一个只含单个方法的接口实现类上。这催生了大量"类只有一个方法,方法体只有几行"的策略类。

一等函数改变了这一切。当一个函数可以像值一样被传递、存储和替换时,策略就不需要是一个类。

从接口层级到可调用对象

经典策略模式在 Python 中的标准写法仍然常见:

python 复制代码
from abc import ABC, abstractmethod

class DiscountStrategy(ABC):
    @abstractmethod
    def apply(self, order): ...

class FidelityPromo(DiscountStrategy):
    def apply(self, order):
        return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0

class BulkItemPromo(DiscountStrategy):
    def apply(self, order):
        # ... 省略

引入函数对象后,DiscountStrategy 抽象类、两个实现类、以及 Context 中持有策略对象的样板全部消失:

python 复制代码
from typing import Callable, Optional

def fidelity_promo(order) -> float:
    return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0

def bulk_item_promo(order) -> float:
    # ...

class Order:
    def __init__(self, ..., promotion: Optional[Callable] = None):
        self.promotion = promotion

    def due(self):
        discount = self.promotion(self) if self.promotion else 0
        return self.total() - discount

promotion 只是一个实例属性,恰好持有一个可调用对象。调用时写 self.promotion(self)------第一个 self 取出属性,第二个 self 作为参数传入。没有抽象基类,没有继承层级,每个策略就是一个普通函数,可以独立测试,也可以随时替换。

导航系统的例子更清晰地暴露了"分支选择算法"的问题:

python 复制代码
# 算法选择条件写在上下文内部:新增"骑行"就要修改 build 方法
class Navigator:
    def build(self, mode, start, end):
        if mode == "car":
            return [start, "highway", end]
        elif mode == "walking":
            return [start, "park path", end]

用 Callable 替代后,上下文完全不需要知道存在哪些算法,切换策略只是一次赋值:

python 复制代码
Route = Callable[[str, str], list[str]]

def car_route(start, end): return [start, "highway", end]
def walking_route(start, end): return [start, "park path", end]

class Navigator:
    def __init__(self, strategy: Route):
        self.strategy = strategy
    def build(self, start, end):
        return self.strategy(start, end)

nav = Navigator(car_route)
nav.strategy = walking_route  # 运行时替换,一次赋值

Java 的演进方向提供了佐证:Predicate<T>、Function<T,R> 这类函数式接口加上 lambda,已经让"现代 Java 中策略模式更多是一种特性,而不是需要付出大量努力才能实现的模式"。当语言本身提供了函数值,再把每个策略做成一个类,就是在为不存在的问题付出维护成本。

一个必要的边界

函数对象不是无条件更好的。如果"策略"需要携带显著的状态 ,或者需要在运行时读取和修改自身配置 ,一个轻量的类仍然合理。区分标准不是"模式 vs 函数",而是:这个行为是否真的需要独立的身份和生命周期。一个只做纯计算的策略函数,没有任何理由拥有一个类身份。

用语言特性取代"包装与构建"模式

Decorator 和 Builder 解决的是另一个维度的问题:前者处理"在不修改对象的前提下叠加行为",后者处理"构造函数参数失控"。这两种需求在现代语言中都有了比模式更直接的表达。

装饰器语法 ≠ 装饰器模式,而且它通常更好用

Python 的 @decorator 语法和 GoF 的 Decorator 模式共享一个名字,但作用机制不同,而正是这种不同让它更具替代价值。Decorator 模式在运行时用包装对象层层包裹一个已有对象,依赖共同的接口来保持类型兼容;Python 装饰器在函数或类定义时直接修改被装饰的可调用对象,是一个更高层的语言构造。

经典 Decorator 模式在 Python 中往往被写成这样:

python 复制代码
class Coffee: ...
class MilkDecorator:
    def __init__(self, coffee): self._coffee = coffee
    def cost(self): return self._coffee.cost() + 2
class SugarDecorator:
    def __init__(self, coffee): self._coffee = coffee
    def cost(self): return self._coffee.cost() + 1

用装饰器语法,意图被直接声明在函数定义处:

python 复制代码
def with_milk(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs) + 2
    return wrapper

def with_sugar(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs) + 1
    return wrapper

@with_milk
@with_sugar
def coffee():
    return 10

coffee() 返回 13。没有包装类,没有 self._coffee 的层层委托,装饰的组合关系在函数定义的头部一目了然。在 Java 中,同样的思路以函数组合的形式出现:increment.andThen(double) 用一个 andThen 链替代了包装器类的嵌套。装饰器模式原本的意图------动态叠加行为------被保留了下来,但实现从"对象图管理"降级成了"函数组合",后者的认知负担和出错面都小得多。

需要澄清的是,Python 装饰器语法并不排斥运行时装饰:你仍然可以在运行时把函数交给一个高阶函数。但当装饰关系在定义时就是已知的,用 @ 语法把意图写在签名旁边,比在调用方手工组装装饰器对象链要直接得多。

关键字参数与 kw_only:让 Builder 失去主要存在理由

Builder 模式最常见的动机是"构造函数参数爆炸":一个 __init__ 有十个以上的位置参数,调用方写出 House("brick", "tile", True, False, True, 2, 12),没有人能记住第三个 True 对应的是游泳池还是车库。在 Java 中,这确实需要 Builder 来提供命名清晰的逐步设置。

但在支持关键字参数的语言中,Builder 的主要动机被语言本身直接消除了:

python 复制代码
from dataclasses import dataclass

@dataclass(kw_only=True)
class House:
    walls: str
    roof: str
    floors: int = 1
    windows: int = 4
    has_pool: bool = False
    has_garden: bool = False
    has_garage: bool = False

house = House(walls="brick", roof="tile", floors=2, has_garage=True)

每个参数在调用点都是自文档化的,可选字段有默认值,顺序无关,不想要的字段直接省略。这正是 Builder 在 Java 中试图通过 setMaxTemperature(t)、setMinTemperature(t) 等方法达成的效果,但在 Python 中由语言构造直接完成。甚至不需要手写 __init__:kw_only=True 的 dataclass 在声明层面就强制了命名传参。

Builder 模式只在一种情况下仍然值得保留:构建过程本身是有序的、多阶段的,且阶段之间存在验证,或者同一个构建序列需要产出不同的表示。如果需求只是"参数多、可读性差",关键字参数是比 Builder 低一个数量级的解决方案。

判断框架:什么时候该放下模式

避免滥用不是走向另一个极端------把模式从工具箱里彻底扔掉。模式作为团队沟通的术语仍然有价值,真正的领域复杂性(多阶段构建、需要维护状态的行为族、跨对象的复杂协作)也仍然需要结构化的设计。一个可操作的判断顺序是:

第一问:这个模式封装的是"行为"还是"事物"? 如果核心是一个单方法接口,先问语言是否允许直接传递函数。允许,就用函数;只有当行为需要携带可变状态或独立生命周期时,才考虑类。

第二问:语言是否已经提供了直接的语法来表达这个意图? 装饰关系用 @ 语法或函数组合;参数组合用关键字参数和默认值;接口实现用 Callable 或函数类型。如果语法直接覆盖了模式 80% 的用例,剩余 20% 才需要模式。

第三问:去掉抽象层之后,代码的"读"变难了还是变简单了? 一个不需要画类图就能读懂的方案,通常优于一个需要三跳才能找到实际逻辑的方案。设计模式的价值在于降低理解成本,如果一个模式让理解成本上升,它就已经背叛了自己的目的。

最终,好代码的标准不是"用了几个模式",而是"一个熟悉这门语言的程序员能否在不需要额外解释的情况下读懂意图"。当语言已经能把意图直接写在代码里时,再用模式包裹一层,只是在用额外的抽象为语言的进步缴税。