为什么 16 个设计模式在动态语言里消失了
设计模式不是编程的真理,而是语言表达能力不足的旁证——理解了这一点,才算真正理解了设计模式。
一个让 16 个模式消失的发现
1996 年,Peter Norvig 做了一件让面向对象世界震颤的事。他把 GoF 的 23 个经典设计模式逐个放在 Lisp 和 Scheme 等动态语言中重新审视,发现了一个令人尴尬的事实:23 个模式中,有 16 个在动态语言里要么直接消失,要么退化成三五句代码。
这不是因为 Lisp 程序员更聪明。Norvig 的论文揭示了一个更深层的规律:设计模式的存在,与语言的抽象能力成反比。语言越强大,模式越少。每一代更强的语言,都在消灭上一代赖以生存的设计模式——而这个过程不可逆。
模式的三层坍缩
Norvig 将模式分为三个递进的层次。惯用法(Idiom)是语言级别的模式,比如 C 语言里用 do-while(0) 包装宏。设计模式(Design Pattern)是编程级别的模式,也就是 GoF 的那 23 个。架构模式(Architectural Pattern)是系统级别的模式,比如 MVC、微服务——三者的区别只在于抽象粒度不同。
关键洞察在于:随着语言抽象能力的提升,高层模式会"下沉"为更低层的惯用法,甚至直接变成语言特性。递归曾经在 Lisp 中是需要刻意练习的编程技巧,现在成了所有现代语言的基本能力。垃圾回收曾经是程序员手工管理的"架构模式",现在成了运行时的标配。
这个过程只有一个方向:从复杂到简单,从需要记忆到自然消失。
动态语言的四把刀
那么,动态语言究竟凭什么消灭了这些模式?Norvig 指出了四种核心能力。
一等函数消灭了 Strategy、Command、Template Method、Observer 等模式。在 Java 里,你需要定义一个 Strategy 接口、写一个具体实现类、再通过构造函数注入——三个文件协作才能完成策略切换。在支持一等函数的语言里,你只是传了一个函数。不是模式简化了,是问题根本不存在了。
正如函数式编程所揭示的:缺乏一等函数是 Java 需要那么多设计模式的主要原因。一旦语言把函数当作一等公民,Strategy、Command、Template Method 的区别就只剩下语义层面的选择,不再需要专门的类结构来支撑。
宏与元编程消灭了 Abstract Factory、Builder、Decorator 等结构性模式。在Lisp中,宏可以在编译期对语法树进行任意变换——你不需要用类继承来"模拟"新语法,可以直接创造新语法。Decorator 模式在 Lisp 中就是一个宏调用,不需要一层又一层的包装类。
动态类型消灭了 Adapter、Bridge、Proxy 等接口适配模式。静态语言要求编译期类型匹配,所以需要适配器把一个接口"翻译"成另一个。动态语言奉行鸭子类型:只要对象有这个方法,就能调用,不需要声明接口。类型检查的负担从编译器转移到了运行时,适配器就失去了存在的理由。
闭包消灭了需要"封装状态"的模式。Command 模式的核心是把"请求"封装成对象,让它携带状态。但闭包天然地捕获变量——一个匿名函数加上它引用的环境,就是 Command 模式的全部。不需要类,不需要接口,不需要 execute() 方法。
16 个模式消失或简化,不是 Lisp 的魔法,而是表达能力足够强时,复杂度的自然坍缩。
模式是语言缺陷的证据
把 Norvig 的发现推到极致,会得到一个锐利的判断:设计模式是语言设计缺陷的 workaround。
把 Norvig 的发现推到软件工程的一般实践,会得到一个更锐利的判断:大部分 GoF 设计模式,本质上是语言设计缺陷的补救方案。每一种模式,都在暗示"我的语言在这个地方表达能力不够"。Visitor 模式暗示语言不支持多分派(multiple dispatch),Strategy 模式暗示函数不是一等公民,Adapter 模式暗示类型系统太僵化。
这也能解释一个有趣的现象:设计模式在 Java 社区被讨论得最多,在 Python、JavaScript 社区则很少被提及。不是 Java 程序员更懂设计,而是 Java 的表达能力恰好需要这些补丁。
但这个判断需要一个重要的限定:消失的是"实现模式",不是"设计思想"。Observer 模式里"当一个对象状态改变时通知依赖者"这个思想,在任何语言中都有价值。只是当你有一等函数时,不需要为它建三层类继承。编程语言设计中有一个被低估的价值:好的语言不是给你更多工具来解决复杂问题,而是让复杂问题不再复杂。
下一回你在白板上画 UML 类图,试图用三层继承实现策略切换时,停下来问一句:如果我能直接传一个函数,这些类还需要存在吗?