模式的世界观
小李的第三个年头
窗外的北京正在经历入冬以来最冷的一周。凌晨两点,程序员小李盯着屏幕上的代码,手指悬在键盘上方,却迟迟敲不下去。显示器上是一片深红色的错误堆栈,蜿蜒曲折,像一条永远找不到出口的迷宫。他已经在这家中型互联网公司工作了三年,维护着一套承载公司核心业务的后台系统。
这套系统是公司成立之初就上线的,代码量已经超过了八十万行。当年的开发者早已离职,留下的是一份没有任何注释、变量命名混乱、逻辑纠缠成团的"遗产"。小李依稀记得自己入职第一天,CTO拍着他的肩膀说:"小李,这套系统就交给你了,它是公司的命脉。"
三年过去了,小李从一个意气风发的年轻工程师,变成了一个疲惫不堪的"救火队长"。他每天的工作就是应对一个又一个突如其来的bug——修复一个支付问题,可能引发库存系统的异常;调整一个报表功能,可能让用户权限验证彻底崩溃。他感觉自己像是在一艘四处漏水的船上做船员,每次补好一个洞,就会有两个新的洞出现。
反复出现的困境
小李的处境并非个例。在硅谷,在中关村,在全球每一个有程序员的地方,类似的剧情每天都在上演。2019年,Stack Overflow对全球近九万名开发者进行了一项调查,结果显示,超过62%的受访者表示他们每周至少要花费10小时以上的时间来处理"技术债务"——那些为了赶工期而留下的临时性代码、那些为了快速上线而忽略的架构设计问题。超过78%的开发者认为,技术债务严重影响了他们的开发效率和代码质量。
这个问题并非始于今日。如果我们追溯软件工程的历史,会发现一个令人沮丧却又意味深长的现象:同样的错误,人类似乎在反复重犯。1968年,在 NATO 软件工程会议上,来自全球的计算机科学家们第一次提出了"软件危机"这个概念,他们忧心忡忡地讨论着软件项目如何延期、超预算、以及最终交付无法使用的系统。五十年后的今天,这些问题依然存在,甚至在某些方面变得更加严重。
但这并不意味着我们毫无进步。事实上,正是这些反复出现的困境,推动着一代又一代的软件工程师去思考、去总结、去寻找解决方案。他们开始意识到:那些导致项目失败的问题、那些让代码难以维护的陷阱、那些造成系统脆弱的设计,其实是有规律的。如果能够识别这些规律,并针对每一种规律找到经过验证的解决方案,那么软件开发就不再是盲目的试错,而成为一门可以学习和传承的技艺。
这种认识催生了一个重要的概念——设计模式。
模式:反复出现的问题与反复有效的解决方案
要理解什么是设计模式,我们不妨从身边的例子说起。想象一下你去一家餐厅点餐。无论这家餐厅是川菜馆、粤菜馆还是西餐厅,菜单的结构大致都是相似的:你先选择开胃菜,然后是主菜,最后是甜点或饮品。厨师在准备这些菜品时,也有固定的流程——切配、烹饪、装盘。这种结构化的点餐和烹饪方式,就是一种"模式"。它不是某一家餐厅的发明,而是餐饮行业在长期发展过程中形成的、被证明行之有效的做事方式。
在软件领域,模式(Pattern)同样是指那些在特定情境下反复出现的问题,以及针对这些问题反复有效的解决方案。模式不是具体的代码或算法,而是一种更高层次的抽象——它是关于"在什么情况下、应该采用什么结构、为什么这样设计"的指导原则。
Christopher Alexander是一位建筑师,他在1977年出版的《建筑的永恒之道》(*A Pattern Language*)一书中,系统地阐述了他对模式的理解。Alexander认为,模式存在于我们生活的各个层面——从城市布局到建筑设计,从房间装修到家具摆放,每一种好的模式都描述了一个反复发生的问题,以及解决这个问题的方法。他提出了一个著名的定义:"每一个模式都是一个由三部分组成的规则,它表达了一个特定情境中反复出现的问题,以及解决这个问题的方法的核心,使你能够无数次地使用这个解决方案,而不必每次都重新发明。"
这个定义后来被软件行业广泛采纳。Gamma、Helm、Johnson和Vlissides在他们的经典著作《设计模式:可复用面向对象软件的基础》(*Design Patterns: Elements of Reusable Object-Oriented Software*)中,对软件领域的模式进行了系统的分类和描述。他们指出,设计模式的核心价值在于:它不是凭空想象出来的"最佳实践",而是经过无数实际项目验证的、被证明在特定情境下确实有效的解决方案。每一种模式都解决了一个具体的问题,都有着明确的适用条件和实现细节,都经过了时间的检验。
GoF:四人组的传奇
1994年,对于整个软件行业来说,是一个值得铭记的年份。在这一年,四位来自学术和工业界的软件工程师在Addison-Wesley出版社出版了一本薄薄的书,书的封面上印着四个人的卡通形象——Erich Gamma、Richard Helm、RALPH Johnson和John Vlissides。他们后来被业界称为"Gang of Four",简称GoF,中文译作"四人帮"或"GoF四人组"。
Erich Gamma,瑞士人,曾在苏黎世联邦理工学院获得计算机科学博士学位。在写作这本书时,他是Object Technology International(OTI)公司的技术总监,这家公司是IBM的子公司,专注于面向对象技术的研究和应用。Gamma在软件工程领域有着深厚的学术背景和丰富的实践经验,他参与过多个大型软件项目的开发,对代码复用和软件架构有着深刻的理解。
Richard Helm,同样来自澳大利亚的昆士兰大学,他在那里获得了博士学位,并在IBM的OTI公司工作。Helm专注于面向对象设计的研究,他对模式的研究始于一个看似简单却又深刻的问题:为什么有些面向对象代码能够优雅地应对变化,而有些代码却脆弱得不堪一击?
Ralph Johnson是伊利诺伊大学厄巴纳-香槟分校的教授,他长期从事软件工程和面向对象技术的研究。Johnson是四人组中学术气息最浓厚的一位,他的研究方向包括软件架构、设计模式、以及面向对象设计原则。他对模式的研究有着独特的视角:模式不仅仅是技术的解决方案,更是一种沟通的语言,一种在开发者之间传递设计经验和智慧的工具。
John Vlissides,哥伦比亚大学的博士,后来成为IBM T.J. Watson研究中心的研究员。Vlissides是四人组中最善于思考和写作的一位,他为模式的概念化和传播做出了重要贡献。在GoF的书中,许多模式的名字和分类方式都出自他的构思。
这四个人的背景各有不同——有人来自学术界,有人来自工业界;有人擅长理论研究,有人精通实际开发。但他们有一个共同点:都在长期的软件开发实践中,发现了同样的问题——软件开发者在面对某些反复出现的设计挑战时,往往需要"重新发明轮子",而实际上这些问题的解决方案早已存在于某些经验丰富的开发者的头脑中,只是没有被系统化地整理和传播。
他们决定改变这种状况。
那次改变了软件历史的会议
GoF四人组的相遇并非偶然。1989年,Kent Beck和Ward Cunningham在OOPSLA(面向对象编程、系统、语言和应用会议)上发表了一篇论文,提出了将模式应用于软件设计的想法。这篇论文像一颗种子,播撒在了软件行业的土地上。
两年后的1991年,Kent Beck在德国Königswinter举办的一次小规模会议上,向几位同行提出了一个大胆的想法:为什么不像建筑师Christopher Alexander那样,为软件设计也创建一套模式语言?这个想法得到了热烈的响应,其中包括了当时已经在这个领域有所思考的Erich Gamma和Richard Helm。
1992年,Pattern Languages of Programming(PLoP)会议首次举办,这是软件模式研究领域的一个重要里程碑。来自世界各地的研究者和实践者聚集在一起,分享他们在模式方面的思考和经验。正是在这个会议上,Gamma、Helm、Ralph Johnson和John Vlissides这四位背景各异的开发者相遇了,并开始讨论一个宏大的计划:系统地整理和分类面向对象软件设计中的模式。
这个计划持续了将近三年。他们分析了大量的现有代码库,研究了众多成功的面向对象设计案例,参考了其他领域(特别是建筑学)的模式理论,并与全球各地的同行进行了广泛的交流和讨论。最终,他们从数百种候选模式中精选出23种,编纂成了那本改变软件行业历史的著作。
1994年秋天,《设计模式》一书正式出版。书的副标题是"可复用面向对象软件的基础"(*Elements of Reusable Object-Oriented Software*),这个标题直接致敬了Alexander的《建筑的永恒之道》。书中共收录了23种设计模式,分为三大类:创建型模式(Creational Patterns)、结构型模式(Structural Patterns)和行为型模式(Behavioral Patterns)。
这本书的影响力远远超出了四位作者的任何预期。在出版后的几年里,它迅速成为软件工程领域被引用最多的著作之一。据统计,到2020年为止,该书已经被翻译成20多种语言,全球销量超过100万册。更重要的是,它开创了一个全新的软件工程分支——模式研究。Pattern Languages of Programming(PLoP)会议至今仍在举办,专门讨论模式的各种会议和研讨会遍布全球,各种模式相关的书籍和文章层出不穷。
模式究竟是什么
让我们回到小李的故事。三年后的一个下午,小李偶然在网上看到了一篇关于设计模式的博客。文章的作者讲述了自己在维护一个复杂系统时,如何通过学习和应用设计模式,将原本纠缠不清的代码重构得清晰有序。小李感到一种久违的兴奋——也许,他苦苦寻找的答案就在这本书里。
但当他真正拿起GoF的原著时,却发现这本书并不好读。四位作者使用了大量的专业术语,描述也相当抽象,很多章节读完之后,小李仍然不知道应该如何将书中的理论应用到自己的代码中。他开始怀疑:这些"模式"真的有用吗?还是只是学者们茶余饭后的空谈?
小李的困惑代表了许多初学者面对设计模式时的真实感受。这并不奇怪——GoF的书写于三十年前,那时候大多数开发者还在使用C++这样的语言,而今天主流的编程语言如Java、Python、JavaScript等,在语法和特性上都有了很大的变化。更重要的是,书中描述的许多问题和建议,在当时是革命性的洞见,但经过三十年的传播和应用,有些已经成为了现代编程语言和框架的内置特性,有些则需要根据新的编程范式进行调整。
但这并不意味着模式已经过时了。恰恰相反,模式所蕴含的核心思想——在特定情境下反复出现的问题,以及针对这些问题反复有效的解决方案——永远不会过时。模式不是具体的代码实现,而是一种解决问题的思路,一种在设计层面进行思考的方式。
为了更好地理解这一点,让我们通过一个具体的例子来说明。
一个关于对象的创建问题
假设你在开发一个电商系统,其中有一个功能是创建各种类型的订单。用户可以创建普通订单、团购订单、秒杀订单、预售订单等等。在系统初期,你可能这样写代码:
```java public class OrderService {
public Order createOrder(String type) { if ("normal".equals(type)) { return new NormalOrder(); } else if ("group".equals(type)) { return new GroupOrder(); } else if ("seckill".equals(type)) { return new SeckillOrder(); } else if ("presell".equals(type)) { return new PresellOrder(); } throw new IllegalArgumentException("Unknown order type: " + type); } } ```
这段代码看起来简单直接,但随着业务发展,问题会逐渐显现。首先,如果你需要添加新的订单类型,就必须修改这个方法,违反了对修改关闭的原则。其次,每种订单的创建逻辑可能包含很多细节(比如初始化库存、计算价格、发送通知等),随着这些逻辑变得越来越复杂,这个方法会变得臃肿不堪。第三,如果有多个地方需要创建订单,而创建逻辑需要调整,你就不得不在多个地方进行修改,极易引入bug。
面对这个问题,GoF四人组在他们的书中描述了一种模式来解决它——工厂方法模式(Factory Method Pattern)。这种模式的核心思想是:将对象的创建封装到一个专门的"工厂"类中,客户端代码不需要知道具体创建的是哪一种订单,只需要调用工厂的方法,工厂会负责根据参数或配置来决定创建哪种具体的订单对象。
使用工厂方法模式重构后的代码可能是这样的:
```java // 订单接口 public interface Order { void init(); void calculatePrice(); void notifyUser(); }
// 普通订单 public class NormalOrder implements Order { @Override public void init() { /* 初始化普通订单 */ } @Override public void calculatePrice() { /* 计算普通订单价格 */ } @Override public void notifyUser() { /* 发送普通订单通知 */ } }
// 团购订单 public class GroupOrder implements Order { @Override public void init() { /* 初始化团购订单 */ } @Override public void calculatePrice() { /* 计算团购价格 */ } @Override public void notifyUser() { /* 发送团购订单通知 */ } }
// ... 其他订单类型
// 订单工厂 public abstract class OrderFactory { public abstract Order createOrder(); }
// 具体工厂 public class NormalOrderFactory extends OrderFactory { @Override public Order createOrder() { return new NormalOrder(); } }
public class GroupOrderFactory extends OrderFactory { @Override public Order createOrder() { return new GroupOrder(); } }
// 客户端代码 public class OrderService {
public Order createOrder(OrderFactory factory) { Order order = factory.createOrder(); order.init(); order.calculatePrice(); order.notifyUser(); return order; } } ```
重构后的代码看起来更复杂了——确实,引入模式会增加一些结构上的开销。但这种复杂性的增加是有价值的:首先,新增订单类型只需要添加新的类和新的工厂类,不需要修改原有的代码;其次,每种订单的创建逻辑被封装在对应的工厂类中,代码的职责更加清晰;第三,客户端代码不再依赖于具体的订单类,而是依赖于抽象的Order接口和OrderFactory抽象类,这使得代码更加灵活,更容易进行单元测试。
这正是模式的本质:它不是在代码中简单地"应用"某种技术,而是一种经过深思熟虑的设计决策。这种决策带来的是更好的代码结构、更强的可维护性、以及对变化的更好适应。
模式的三个层次
GoF的23种模式固然经典,但它们只是软件模式世界中的一小部分。实际上,模式存在于软件开发的各个层面。
在最低的层面,我们有代码层面的模式,这些模式关注的是具体的代码实现细节,比如如何组织一个类的方法、如何命名变量、如何处理错误等。这些模式类似于编程中的"风格指南",帮助开发者写出更加清晰和一致的代码。
在中间层面,我们有面向对象设计模式,也就是GoF书中主要讨论的内容。这些模式关注的是如何组织类和对象之间的关系,如何通过继承、组合、委托等机制来构建灵活和可扩展的系统。这些模式是本书的核心内容。
在最高的层面,我们有架构模式,这些模式关注的是整个系统的宏观结构。比如著名的MVC(模型-视图-控制器)模式描述了如何将用户界面、业务逻辑和数据存储分离;微服务架构描述了如何将一个大型系统分解为多个独立部署的服务;事件驱动架构描述了如何通过异步消息传递来构建松耦合的系统。
除了这三个层次,模式还存在于特定的领域和应用场景中。比如在并发编程中,有各种线程安全模式、锁策略模式、线程池模式等;在用户界面开发中,有MVP、MVVM等表现模式;在数据库应用中,有各种数据访问模式、缓存模式、事务管理模式等。
```mermaid graph TD A[模式的层次] --> B[架构模式] A --> C[设计模式] A --> D[代码模式]
B --> B1[MVC架构] B --> B2[微服务架构] B --> B3[事件驱动架构]
C --> C1[创建型模式] C --> C2[结构型模式] C --> C3[行为型模式]
D --> D1[命名规范] D --> D2[注释规范] D --> D3[错误处理] ```
理解模式的层次性非常重要。它提醒我们,模式不是万能的——每种模式都有其适用范围,超出这个范围强行使用,反而会带来不必要的复杂性。同时,它也提醒我们,不应该把模式看得过于神秘——它们不是什么高深莫测的理论,而是无数开发者经验的结晶,是可以学习和掌握的实用工具。
模式学习的正确姿势
小李在继续学习设计模式的过程中,逐渐摸索出了一些