设计模式入门:从'能用'到'好用'
设计模式入门:从“能用”到“好用”
凌晨两点,程序员小李盯着屏幕上的代码,眉头皱成了麻花。
三天前他刚接了一个紧急需求——给订单系统加一个退货功能。需求不复杂,数据库里加几个字段,后端改几行逻辑,前端调一下接口,两天搞定,测试通过,上线。
结果第三天早上,同事老张找到他,说:“小李啊,你那个退货模块我看了一下午,没太看懂。你能给我讲讲吗?”
小李愣了一下:“就那么点代码,有什么好讲的?”
老张苦笑着指了指屏幕:“你看啊,这边判断订单状态,那边又改了一次,过了一会儿又判断了一次。我跟了半天都不知道什么时候用哪个状态。你这个状态流转,能不能画个图给我?”
小李无言以对。他自己写的时候倒是很清楚,但回过头来看,确实有点乱。更让他意外的是,接下来一周,陆续有三个同事找他问这块代码怎么改,因为需求又变了——这次不是退货,而是换货。
每一次,小李都得花半小时重新梳理自己的代码,然后告诉对方:“这里改一下,那里动一动,应该就能跑。”然后看着对方一脸茫然的样子,心里开始怀疑人生。
这个场景,可能在每个程序员的职业生涯里都出现过。代码明明能跑,功能实现了,测试也通过了——但为什么别人(包括一段时间后的自己)看起来就像看天书?为什么每次需求变更都要大动干戈?为什么一个看似简单的改动会引发意想不到的连锁反应?
这就是本章要回答的核心问题:什么是好的代码?为什么“能用”还不够?设计模式到底能帮我们做什么?
---
代码的“烂”与“好”:三个维度看懂代码质量
讨论代码好不好之前,先得有个评判标准。就像评价一道菜好不好吃,有人说“味道不错”,有人说“太咸了”,没有统一尺度就没法讨论。
对于代码质量,业界有一套相对成熟的评估框架,其中最核心的三个维度是:可读性(Readability)、可维护性(Maintainability) 和 可扩展性(Extensibility)。这三个词听起来抽象,但理解了它们对应的真实痛苦,你就会觉得一点都不虚。
可读性:代码是写给人看的,顺眼的才好看
“可读性”这个词听起来像是写小说的要求,但代码的可读性比小说还重要。小说读不懂,你可以翻回去再看一遍;代码读不懂,你连它干了什么都不确定,改起来就是在冒险。
什么是可读性差的代码?让我们看一个具体的例子。下面这段代码实现了根据用户类型计算折扣的业务逻辑:
```python
某电商项目中的折扣计算代码(示例)
def calc_price(user_type, original_price, is_vip, order_count): if user_type == 1: if is_vip: return original_price * 0.7 else: if order_count > 10: return original_price * 0.85 else: return original_price * 0.9 elif user_type == 2: if order_count > 5: return original_price * 0.75 else: return original_price * 0.8 else: return original_price ```
这段代码功能上没有问题——能跑,能算出结果。但它的可读性存在严重问题。问题在哪?
首先,魔法数字遍地都是。`user_type == 1` 是什么意思?`0.7` 是七折还是别的什么?数字本身不携带任何业务含义,读者必须靠上下文猜测。其次,嵌套层次过深。最多三层 `if-else` 嵌套,每嵌套一层,理解成本就指数级上升。第三,函数名语义模糊。`calc_price` 没有说明是什么类型的计算,`user_type` 参数到底有哪些合法值?这些信息完全依赖读者的背景知识。
我们换一种写法:
```python class UserType: REGULAR = "regular" SILVER = "silver" GOLD = "gold"
class DiscountPolicy: """折扣策略:定义不同用户类型的折扣规则"""
@staticmethod def calculate_price(user_type: str, base_price: float, is_vip: bool, total_orders: int) -> float: """计算用户应付金额
Args: user_type: 用户类型 (regular/silver/gold) base_price: 商品原价 is_vip: 是否为VIP会员 total_orders: 历史订单总数 """ price = base_price
if user_type == UserType.REGULAR: if is_vip: price = base_price * DiscountRate.VIP_DISCOUNT # VIP七折 elif total_orders > 10: price = base_price * DiscountRate.LOYAL_CUSTOMER # 老客户八五折 else: price = base_price * DiscountRate.NEW_CUSTOMER # 新客户九折
elif user_type == UserType.SILVER: price = base_price * ( DiscountRate.SILVER_BULK if total_orders > 5 else DiscountRate.SILVER_STANDARD )
elif user_type == UserType.GOLD: price = base_price * DiscountRate.GOLD_DISCOUNT # 金牌会员固定折扣
return price ```
这个版本用了常量类来消除魔法数字,用类型别名让参数语义清晰,用注释说明了业务规则。虽然行数变多了,但阅读者不需要在脑子里做“数字→含义”的翻译,能直接理解代码在做什么。
代码的可读性决定了团队协作的效率上限。一个月后让你自己来维护这段代码,前一个版本你可能需要半小时才能回忆起来逻辑,后一个版本可能五分钟就够了。在一个持续迭代的项目里,这种差距累积起来的时间成本是惊人的。
可维护性:改一处不坏另一处,才叫维护得动
可维护性是代码质量的核心战场。可读性解决的是“看不看得懂”的问题,可维护性解决的则是“改起来安不安全”的问题。
很多初级程序员有个误区:代码写完能跑,任务就完成了。至于以后要不要改、怎么改,那是以后的事。这种想法在真实的项目环境中几乎必然翻车。需求不会等你写完再变,业务不会等你优化完再发展,bug也不会等你准备好了再来。
真正考验代码质量的不是正常流程,而是变化。
还是刚才那个折扣场景。假设三个月后,产品经理说:“我们新增一种用户类型叫'企业用户',企业用户如果订单金额超过一万,全单八折;如果低于一万,九五折。而且企业用户不享受VIP折扣。”你怎么改?
在前一个版本(可读性差的那个)里,你得小心翼翼地在层层嵌套的 `if-else` 里找到一个合适的位置,把新逻辑塞进去。问题是:这个新逻辑的判断条件和其他条件有没有交叉?会不会出现两个分支都满足的情况?改了之后会不会影响已有的测试用例?每一次修改都像在雷区里走路。
在后一个版本里,修改的空间相对清晰——在 `UserType` 里加一个新类型,在 `DiscountPolicy` 里加一个 `elif` 分支,逻辑独立,不影响现有分支。改动量可控,影响范围可预期。
这就是可维护性的本质:代码的改动成本应该与改动范围成正比,而不是与系统规模成正比。 好维护的代码,改一个功能,影响三五行;难维护的代码,改一个功能,可能引爆十个不相关的模块。
怎么衡量可维护性?业界有几个常用指标。一个是 圈复杂度(Cyclomatic Complexity),指的是代码中独立路径的数量——路径越多,说明逻辑越复杂,越难测试,越容易藏 bug。另一个是 耦合度(Coupling),指的是模块之间的依赖关系强度——高耦合意味着改一个模块会牵扯一堆其他模块,牵一发而动全身。
可扩展性:为未知的未来留一扇门
可扩展性是可维护性的远期版本。可维护性关注的是当下的改动成本,可扩展性关注的是系统对未来变化的适应能力。
回到折扣的例子。三个月前的产品需求可能只考虑了个人用户打折,现在的业务已经扩展到企业用户。半年后可能还有活动期间的限时折扣、节日主题促销、按地域差异化定价……需求永远不会停。
可扩展性好的代码是怎么处理这种“不确定性”的?关键在于把变化的部分封装起来,让核心逻辑稳定,让变化的部分可插拔。
在折扣场景里,“变化的部分”是什么?是折扣的计算规则。“稳定的部分”是什么?是“先确定用户身份,再应用对应折扣”的基本流程。如果能把“折扣计算规则”从主流程中分离出来,让它可以独立替换而不影响其他代码,那系统就能优雅地应对需求变化。
这正是设计模式的核心思想之一——通过特定的结构和组织方式,让代码“拥抱变化”而不是“对抗变化”。
读到这里,你可能会想:这些道理听起来都对,但具体怎么做?怎么能把代码写得有可读性、可维护性、可扩展性?有没有一些可以复用的思路和方案?
这正是设计模式要回答的问题。
---
设计模式的起源:从建筑大师到软件工程
“设计模式”这个词在软件行业被广泛使用,但它的思想源头并不在计算机领域,而是在建筑学里。这个追溯很重要——它能帮助我们理解设计模式究竟在解决什么问题,以及为什么这些解决方案值得被反复使用。
Alexander 的模式语言:建筑中的“套路”
20 世纪后半叶,建筑师克里斯托弗·亚历山大(Christopher Alexander)在研究建筑设计时发现了一个有趣的现象:某些空间设计方案在实践中反复出现,而且效果出奇地好——无论是在日本京都的老房子里,还是在纽约的现代公寓中,人们对“好用空间”的感受惊人地一致。
比如,“入口过渡空间”这种模式:一个住宅的入户门直接对着客厅,私密性差,住起来不舒服;但如果门口有一个小的过渡区域(中国的“玄关”或日本的“土间”),形成一个从外部到内部的缓冲,这个空间就好用多了。这种方案在无数建筑中被验证有效,于是亚历山大把它提炼成一个模式,并给它起了个名字:Pattern 1: 入口过渡空间(Pattern 1: Intimacy Gradient)。
亚历山大的核心贡献不仅是发现这些模式,更重要的是他提出了一个框架:模式语言(Pattern Language)。在他看来,任何领域的高手都掌握了一套模式语言——一种由相互关联的模式组成的知识体系。高手的厉害之处不在于凭空发明,而在于知道在什么场景下该用哪种模式,以及这些模式之间如何协作。
1977 年,亚历山大出版了《A Pattern Language》,收录了 253 种建筑模式,从城市尺度(“一条街的活力来自什么”)到房间尺度(“怎么设计一个让人想坐下来的角落”),形成了一个完整的体系。这本书后来被誉为“建筑师的圣经”。
软件行业的借鉴:GoF 与《设计模式》
大约在 20 世纪 80 年代末到 90 年代初,几位软件工程师注意到了亚历山大的工作。其中最著名的四位是埃里克·伽玛(Erich Gamma)、理查德·赫尔姆(Richard Helm)、拉尔夫·约翰逊(Ralph Johnson)和约翰·威利斯德(John Vlissides)。他们当时都在软件领域工作,深感软件设计中存在大量反复出现的结构问题,而且这些问题往往有相似的起因和相似的解决方案。
1994 年,四人在 Addison-Wesley 出版社出版了一本书,书名叫 《Design Patterns: Elements of Reusable Object-Oriented Software》。由于四位作者都是软件工程领域的重要人物,这本书后来被称为 GoF 书(Gang of Four,四人帮),是软件设计模式领域最具影响力的著作,没有之一。
GoF 做了什么?简单来说,他们做了一件和亚历山大类似的事:把软件设计中反复出现的好方案提炼成模式,然后分类整理。
书中提出了 23 种设计模式,每种模式都包含以下几个要素:
- **模式名称(Pattern Name)**:简洁的称呼,方便团队沟通。
- **问题(Problem)**:在什么情况下会遇到这个挑战。
- **解决方案(Solution)**:推荐的结构和组织方式。
- **效果(Consequences)**:使用这个模式带来的利弊权衡。
这种结构化的表述方式让设计模式不仅仅是“经验之谈”,而是变成了可以系统学习、讨论和复用的知识单元。
需要特别指出的是,GoF 的 23 种模式并非凭空发明。其中很多模式在编程实践中已经存在了很长时间——比如面向对象语言中的虚函数机制、多态思想等,支撑了这些模式的技术基础。GoF 的贡献在于把这些散落在实践中的智慧整理成了体系,让后人不用从零摸索。
---
23 种模式的分类体系:一张地图在手
GoF 的 23 种设计模式数量不少,直接啃原文容易迷失方向。好在它们有清晰的分类体系,掌握了这个体系,就相当于拿到了一张地图,知道每种模式大致在解决什么问题、属于哪个领域。
23 种模式按用途分为三大类:创建型(Creational)、结构型(Structural)、行为型(Behavioral)。
创建型模式关注的是“对象怎么创建出来”这件事。乍一看,对象创建不就是 `new` 一个类吗?但在实际项目中,对象的创建过程往往比这复杂得多:有些对象创建需要配置大量参数,有些对象在整个程序运行期间只需要一个实例(单例),有些对象的类型需要在运行时决定(工厂模式)。创建型模式要解决的核心问题就是把“对象创建”的逻辑封装起来,让使用方不需要关心创建细节,降低耦合。
结构型模式关注的是“类和对象怎么组合成更大的结构”。类与类之间的关系、对象之间的组织方式,直接决定了代码的耦合程度和灵活性。结构型模式提供了一系列经过验证的“组合套路”,比如通过接口适配不兼容的类(适配器模式)、把多个对象组合成树形结构(组合模式)、给对象动态地添加功能(装饰器模式)等。
行为型模式关注的是“对象之间的职责分配和算法封装”。当系统中有多个对象需要协作完成一个任务时,谁负责什么、消息怎么传递、决策怎么做,这些问题决定了系统的行为是否清晰、扩展是否方便。行为型模式提供了这些协作关系的最佳实践,比如把不确定的行为封装成可替换的策略(策略模式)、把请求封装成对象从而支持撤销(命令模式)等。
下面这张图展示了 23 种模式在这个三维分类中的位置:
```mermaid graph TD %% 三大类标题 A[📚 设计模式分类体系] --> B[🔨 创建型模式<br/>5种] A --> C[🏗️ 结构型模式<br/>7种] A --> D[🎭 行为型模式<br/>11种]
%% 创建型模式详情 B --> B1[Singleton<br/>单例模式] B --> B2[Factory Method<br/>工厂方法] B --> B3[Abstract Factory<br/>抽象工厂] B --> B4[Builder<br/>建造者模式] B --> B5[Prototype<br/>原型模式]
%% 结构型模式详情 C --> C1[Adapter<br/>适配器模式] C --> C2[Bridge<br/>桥接模式] C --> C3[Composite<br/>组合模式] C --> C4[Decorator<br/>装饰器模式] C --> C5[Facade<br/>外观模式] C --> C6[Flyweight<br/>享元模式] C --> C7[Proxy<br/>代理模式]
%% 行为型模式详情 D --> D1[Chain of Responsibility<br/>责任链模式] D --> D2[Command<br/>命令模式] D --> D3[Interpreter<br/>解释器模式] D --> D4[Iterator<br/>迭代器模式] D --> D5[Mediator<br/>中介者模式] D --> D6[Memento<br/>备忘录模式] D --> D7[Observer<br/>观察者模式] D --> D8[State<br/>状态模式] D --> D9[Strategy<br/>策略模式] D --> D10[Template Method<br/>模板方法] D --> D11[Visitor<br/>访问者模式]
%% 样式 classDef creational fill:#d4edda,stroke:#28a745,color:#155724 classDef structural fill:#cce5ff,stroke:#007bff,color:#004085 classDef behavioral fill:#fff3cd,stroke:#ffc107,color:#856404