去年冬天改一个2018年写的老服务,我在一个工具类里翻到了手写单例——private static volatile Singleton instance,双重检查锁,注释还端正地写着「线程安全」。我盯着看了几秒,把这个类整个删了,换成三行注解 @Component。Spring 启动的时候已经帮我把这件事做完了,二十行代码归零。
那一刻我意识到一件事:GoF 在 1994 年写下的那 23 个模式,到现在有一大半已经被语言本身或者框架吃掉了。不是它们错了,是补丁不需要了。
模式本质上是补丁,不是真理
Erich Gamma 他们四个人写那本书的时候,C++ 还没有 lambda(要等到 2011 年的 C++11),没有垃圾回收,反射能力也弱得可怜。你想把一个「行为」当参数传给另一个函数,做不到——只能定义一个接口,写一个类去实现它,再 new 一个对象传进去。Strategy 模式说白了就是「函数不能当参数」这个限制下的 workaround,不是什么高深思想。
Peter Norvig 在 1996 年做过一个挺出名的分析,把 23 个模式拿到 Lisp 和 Dylan 里过了一遍,结论是其中 16 个要么根本不存在,要么简单到不值得单独写一章。当时不少人觉得他在抬杠。但你现在回头看 Java,会发现同样的事情正在发生,只是换了个语言。
具体被吃掉的那些
Iterator:Java 5(2004 年 9 月)加了 for-each,Python 有 __iter__,JS 有 Symbol.iterator。你现在多久没手写过 hasNext()/next() 了?我大概五年没写过了。
Strategy:Java 8(2014 年 3 月)之后,list.sort(Comparator.comparing(User::getName)) 一行搞定。谁还写那个匿名内部类加接口定义的版本?
Command:本质是把「操作」封装成对象。有了闭包,() -> doSomething() 就够了,不需要 Command 接口 + ConcreteCommand 类 + Invoker 那一整套。
Factory Method:构造函数有默认参数,依赖注入容器管生命周期,百分之九十的场景里那个 Factory 类纯属多余。我上一个项目里四个 Factory,三个只是为了把 new 包起来。
Template Method:父类定骨架、子类填内容。换成高阶函数就是「传个函数进去」,Python 的 key= 参数、JS 的 Array.prototype.map 都是这个思路。
Visitor:为了给已有类型加操作又不改类型定义。Java 21(2023 年 9 月)模式匹配 switch 正式转正之后,很多 Visitor 可以直接改写成 switch 表达式,代码量掉一半以上。
但也不是全死了
Observer 没死,只是改名叫「订阅」、「事件总线」、「响应式流」。你写 Vue 的 watch、RxJS 的 subscribe,用的还是同一套东西,只是没人再画那个 UML 图了。
Adapter 没死,因为集成遗留系统这事儿永远不会消失。我见过一个团队为对接三套老 ERP 写了七个 Adapter,每个都丑得要命,但没它们真不行。
Proxy 也没死,只是藏起来了。你写 @Transactional 的时候不会想到那是动态代理在起作用,写 MyBatis 的 Mapper 接口时也不会。Spring 把这层藏得太好,以至于很多工作三年的工程师从来没意识到自己每天都在用代理模式。
我觉得真正值钱的不是模式本身
是词汇表。
上个月评审代码,我问同事「这块是不是走了策略」,他三秒就懂了。如果换成「这里会根据订单来源走不同的计价分支,而且分支以后还会继续加」,我得说两分钟,他还可能理解偏。
模式提供的是压缩过的沟通单位,不是代码模板。
所以我的建议是反过来的:别先背 23 个名词再找地方套,先看你的代码里哪一块最可能变——是算法在变?还是对象创建过程在变?还是对象之间的通知关系在变?找到变化的方向,抽出来,然后你才配得上那个名字。
硬套模式的结果我见过太多了。一个只有两种支付方式的项目里塞了 AbstractFactory + Strategy + Bridge,三层抽象,改一行逻辑要跳五个文件。那不叫设计模式,那叫 cosplay。