为什么你需要设计模式
第 1 章:为什么你需要设计模式
你有没有过这样的经历:接手了一个"祖传项目",打开代码一看,整个人都傻了——变量名是拼音缩写,函数之间互相调用形成一个巨大的蜘蛛网,修改一个小功能要改动十几个文件,而测试?根本不存在的。这样的代码就像一间堆满杂物的老房子,每次想找点什么都得翻箱倒柜,时间久了,连当初的"设计师"自己都不敢动。
如果你有过这种体验,那么恭喜你,你已经亲身"品尝"过什么叫"没有良好设计的代码"了。如果你没有这种体验,那更值得庆幸——但也别高兴太早,因为在软件开发的漫长旅程中,几乎每个人迟早都会遇到这样的"代码泥潭"。
好消息是,前辈们早就为我们趟过了这条路。他们把无数次踩坑、填坑的经验总结成了一套套可以复用的解决方案,这套方案有一个响亮的名字——设计模式。接下来的内容,就是关于设计模式的入门向导。我们会从实际案例出发,看看糟糕的代码长什么样,聊聊设计模式是怎么诞生的,以及最重要的——它们到底能帮我们解决什么问题。
代码的坏味道:从实际案例看缺乏设计的代码问题
一段"能用但要命"的代码
让我们先来认识一个假想的程序员——小李。他在一家电商公司工作,负责维护一个商品促销系统。这个系统本来很简单:用户提供商品ID,系统计算折扣,返回最终价格。但随着业务发展,系统被不断"打补丁",最终变成了下面这个样子(为了便于说明,我们用简化后的代码展示):
```python class PromotionService: def calculate_price(self, product_id, user_id, order_id, channel, date, is_new_user, has_coupon, coupon_type, coupon_value, is_holiday, promotion_type, ...):
参数列表长得离谱,十几个参数挤在一起
第一层判断:用户类型
if is_new_user: discount = 0.8 else:
第二层判断:渠道来源
if channel == "APP": discount = 0.9 else: discount = 0.95
第三层判断:是否节假日
if is_holiday: discount = discount * 0.9
第四层判断:优惠券
if has_coupon: if coupon_type == "cash": discount = discount * (1 - coupon_value / 100) elif coupon_type == "percent": discount = discount * (coupon_value / 100)
第五层判断:促销活动
if promotion_type == "flash_sale":
秒杀特殊处理...
discount = 0.5 elif promotion_type == "group_buy":
团购特殊处理...
discount = 0.7
第六层判断:订单金额
if order_amount > 1000: discount = discount * 0.95
... 后面还有十几种类似的判断
final_price = original_price * discount return final_price ```
看完这段代码,你有什么感受?恐怕第一反应就是"头大"。这还只是冰山一角——实际上线运行的版本可能有两三千行,各种嵌套的if-else像迷宫一样,还有大量重复的计算逻辑。
这就是所谓的"代码坏味道"。坏味道(Code Smell) 这个词非常形象,它不是说代码有语法错误、不能运行,而是说代码"闻起来不对劲"——虽然能用,但让人看着难受,改起来更难受。
坏味道清单:你遇见过几种?
让我们来盘点一下小李这段代码里暴露出的典型问题:
1. 过长函数(Long Method)
当一个函数塞进了太多功能,它就变成了"瑞士军刀"——什么都能干,但每样都干不好。上述代码中的`calculate_price`函数,承担了计算折扣的所有逻辑,少说也有几百行。这么长的函数,bug调试困难,代码审查更困难。
2. 过大类(Large Class)
如果所有逻辑都堆在一个类里,这个类就成了"万能类"。新需求来了?往里加。临时需求?往里加。久而久之,这个类就变成了无人敢动的"史前巨兽"。
3. 重复代码(Duplicated Code)
仔细看上面的代码,你会发现很多计算逻辑是相似的:每次算折扣都是"原价乘以某个系数"。这种模式重复出现了十几次,却没有抽取成公共方法。一旦业务规则变了(比如折扣系数要改成乘以1.2而不是0.9),你就得改十几处地方,而且很可能漏改某处。
4. 参数列过长(Long Parameter List)
`calculate_price`函数有十几个参数,读代码的时候你得来回对照每个参数的含义。更要命的是,如果将来要加一个新参数(比如"用户会员等级"),你就得改函数签名,然后所有调用这个函数的地方都要跟着改。
5. 条件地狱(Conditional Hell)
层层嵌套的if-else让代码的逻辑变得难以追踪。每一层嵌套都意味着一种新的可能性,测试的时候要覆盖所有分支组合——如果有10层嵌套,每层2个分支,那就有2^10=1024种组合!这简直是个噩梦。
6. 发散式变化(Shotgun Surgery)
当需求变化时,糟糕的代码结构会让你"伤筋动骨"。比如,现在要新增一种促销类型"满减",你得在`calculate_price`里加一个分支,可能还得修改其他几个相关函数。而如果代码结构良好,新增一种促销类型应该只需要新增一个类或一个方法,不会影响现有逻辑。
坏味道的根源:缺乏设计
这些坏味道是怎么产生的?答案很简单:时间压力和需求变化。
刚开始的时候,小李可能只是想:"就这么点需求,先写出来能用就行了。"结果业务越做越大,临时需求越来越多,代码就变成了"意大利面条"——各种逻辑搅在一起,剪不断理还乱。
这就是软件开发的经典困境:写代码容易,改代码难。写代码是创作,改代码是考古——你得小心翼翼地在一堆古董里翻找,生怕碰坏什么。更糟糕的是,代码写的时候是给自己看的,但改的时候往往是给别人看的。如果当初写得混乱,后人维护起来就是"地狱模式"。
那么,有没有什么办法能让我们在写代码的时候就打好基础,避免写出这种"烂代码"?这就是设计模式存在的意义。
设计模式的诞生:GoF四人帮与23个模式的故事
从建筑行业借来的智慧
说到"模式"这个词,你可能会想到建筑行业。建筑师克里斯托弗·亚历山大(Christopher Alexander)在上世纪70年代写了一本叫《建筑模式语言》的书,系统总结了253种建筑设计的"模式"——比如"街道转角应该怎么处理"、"窗户多大最合适"、"阳台应该有多深"等等。这些模式不是强制规定,而是经过大量实践验证的好方案,建筑师可以根据具体情况选择合适的模式。
软件行业的先驱们发现,软件开发其实也有类似的规律:不同的问题反复出现,而聪明的解决方案也可以被总结复用。于是,他们借鉴了亚历山大的思想,开始系统地整理软件开发中的"模式"。
1994年,四位计算机科学家 Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides 合作出版了一本书,书名叫《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)。由于四位作者的姓氏首字母分别是E、G、R、V、V,中文读者形象地称他们为"四人帮"——这其实是《西游记》里妖怪的名字,算是程序员特有的幽默。
这本书一举奠定了设计模式的江湖地位,成为面向对象设计领域的经典之作。书中的23种设计模式被后人称为"GoF模式"(Gang of Four,即四人帮),至今仍是学习设计模式的必读内容。这四位作者也因此被业界尊称为"GoF",地位堪比武侠小说里的"四大宗师"。
23个模式是如何被发现的
你可能会好奇:这23个模式是怎么被总结出来的?难道作者们是坐在办公室里"发明"出来的?
实际上,这些模式大多来自真实的软件开发经验。Gamma等人深入研究了当时许多优秀的面向对象库和框架,比如Smalltalk语言的核心类库、ET++框架等,从中提炼出反复出现的结构设计方案。有些模式甚至是作者们在实际项目中亲身验证过的。
这意味着什么?设计模式不是凭空想象出来的"最佳实践",而是前辈们在无数次实践中"发现"而非"发明"的解决方案。它们是程序员智慧的结晶,是踩过无数坑之后总结出来的血泪教训。
就像牛顿看到苹果落地发现了万有引力,Gamma等人也是在观察大量代码之后,发现了一些反复出现的"结构规律",然后用简洁的语言描述出来,就成了我们今天学习的设计模式。
为什么GoF模式影响如此深远
GoF的书之所以成为经典,关键在于它做到了几点:
首先,标准化了术语。 在这本书出现之前,不同的程序员可能用不同的名字描述同一个概念,比如"装饰器"和"包装器"可能指的是同一个东西。GoF统一了这些术语,让程序员之间的交流更加顺畅。
其次,提供了完整的描述框架。 书中为每个模式都定义了清晰的结构:模式名称、意图、动机、结构、参与者、协作、效果、实现等。这就像给每个"招式"都写了详细的"武功秘籍",让后人学习起来有章可循。
最后,强调了模式背后的设计原则。 模式不是死板的教条,而是灵活的工具。GoF强调,理解模式"为什么"这样设计,比记住"是什么"更重要。只有理解了背后的原理,才能在合适的场景选择合适的模式。
模式的三大分类:创建型、结构型、行为型模式概览
分类的逻辑
GoF的23个模式虽然各有特点,但它们解决的问题领域有所不同。按照模式的主要用途,GoF把这23个模式分成了三大类:
创建型模式(Creational Patterns)——解决"对象如何创建"的问题
结构型模式(Structural Patterns)——解决"对象如何组合"的问题
行为型模式(Behavioral Patterns)——解决"对象如何交互"的问题
这个分类逻辑其实很自然:我们在编程中做的所有事情,归结起来无非就是"创建对象"、"组织对象"、"让对象协作"。设计模式就是针对这三个层面的问题给出的通用解决方案。
创建型模式:对象的"生育问题"
假设你正在开发一个文件浏览器软件,用户可以打开各种文件(文本文件、图片文件、音频文件等)。每种文件都需要对应的"读取器"来处理。你会怎么实现?
最直接的做法是用一堆if-else:
```python def create_reader(file_type): if file_type == "txt": return TextReader() elif file_type == "jpg": return ImageReader() elif file_type == "mp3": return AudioReader()
... 每加一种文件类型就要改这里
```
这样做的问题在于:创建对象的逻辑和使用对象的逻辑混在一起,而且每次加新类型都要改代码。创建型模式解决的就是"如何优雅地创建对象"的问题。
GoF定义了5种创建型模式:单例模式(Singleton)确保一个类只有一个实例,工厂方法模式(Factory Method)把创建对象的责任推迟到子类,抽象工厂模式(Abstract Factory)创建一系列相关对象,建造者模式(Builder)分步骤构建复杂对象,原型模式(Prototype)通过克隆现有对象创建新对象。
结构型模式:对象的"穿衣打扮"
如果说创建型模式关注的是"生娃",那结构型模式关注的就是"穿衣打扮"——如何把类和对象组合成更大的结构。
回到之前的促销系统例子。如果我们想给商品价格增加新的功能(比如日志记录、性能监控),最朴素的做法是直接修改原始类。但这样做有两个问题:修改可能会引入bug,而且如果有多处使用这个类,改动的影响面会很大。
更好的做法是什么呢?结构型模式给了我们思路。比如装饰器模式(Decorator)允许我们动态地给对象添加新功能,就像给它穿上一件件外套——想加功能就"套"一层,不想用就"脱"掉,非常灵活。
GoF定义了7种结构型模式:适配器模式(Adapter)让不兼容的接口能够合作,桥接模式(Bridge)分离抽象和实现,组合模式(Composite)用一致的方式处理单个对象和对象组合,装饰器模式(Decorator)动态添加功能,外观模式(Facade)提供统一入口,享元模式(Flyweight)共享细粒度对象,代理模式(Proxy)控制对对象的访问。
行为型模式:对象的"交流方式"
第三个层面是"对象之间的协作"。比如,一个订单处理系统里,下单后可能要通知库存系统扣库存、通知物流系统准备发货、通知用户系统发送短信。怎么做才能让这些模块松耦合地协作,而不是纠缠成一团乱麻?
行为型模式关注的就是这个问题——如何设计对象之间的通信方式,让它们既能协作又不互相"绑架"。
GoF定义了11种行为型模式,数量最多。比如观察者模式(Observer)定义了对象之间的一对多依赖关系,当一个对象变化时,所有依赖它的对象都会收到通知;策略模式(Strategy)让算法可以互换;命令模式(Command)把请求封装成对象;状态模式(State)让对象的行为看起来随状态改变而改变;责任链模式(Chain of Responsibility)让多个对象都有机会处理请求;模板方法模式(Template Method)定义算法骨架、把具体步骤推迟到子类。
一张图看穿23个模式
为了帮助大家建立全局认知,这里用流程图展示23个模式的分类体系:
```mermaid flowchart TD A["📚 设计模式分类"] --> B["🔨 创建型模式<br/>(5个)"] A --> C["🏗️ 结构型模式<br/>(7个)"] A --> D["🎭 行为型模式<br/>(11个)"]
B --> B1["单例 Singleton"] B --> B2["工厂方法 Factory Method"] B --> B3["抽象工厂 Abstract Factory"] B --> B4["建造者 Builder"] B --> B5["原型 Prototype"]
C --> C1["适配器 Adapter"] C --> C2["桥接 Bridge"] C --> C3["组合 Composite"] C --> C4["装饰器 Decorator"] C --> C5["外观 Facade"] C --> C6["享元 Flyweight"] C --> C7["代理 Proxy"]
D --> D1["观察者 Observer"] D --> D2["策略 Strategy"] D --> D3["命令 Command"] D --> D4["状态 State"] D --> D5["责任链 Chain of Responsibility"] D --> D6["模板方法 Template Method"] D --> D7["迭代器 Iterator"] D --> D8["中介者 Mediator"] D --> D9["备忘录 Memento"] D --> D10["解释器 Interpreter"] D --> D11["访问者 Visitor"]
style A fill:#e1f5fe style B fill:#fff3e0 style C fill:#e8f5e9 style D fill:#f3e5f5 ```
从图中可以看到,23个模式各有分工:创建型模式帮你解决对象"生"的问题,结构型模式帮你解决对象"组装"的问题,行为型模式帮你解决对象"配合"的问题。后续章节我们会逐一详细讲解每个模式,现在只需要对整体框架有个印象就行。
为什么模式有效:经验积累与最佳实践的沉淀
模式的本质:问题的通用解
你可能会问:这些模式凭什么有效?难道写代码的人不能自己想出好方案,非要照搬别人的"套路"?
这个问题触及了设计模式存在的根本原因。软件开发虽然是一种创造性活动,但它并不是从零开始的。大部分时候,我们遇到的问题都有"套路"可循——比如,如何解耦两个模块、如何让代码易于扩展、如何处理对象的创建过程。这些"套路"之所以有效,是因为它们已经经过无数次验证,证明确实能解决问题。
打个比方:做饭需要创意,但刀工、火候、调味的基本技巧是可以总结学习的。一个优秀的厨师掌握了很多"模式"——比如炒青菜要大火快炒、红烧肉要先炒糖色、炖汤要先焯水。这些模式不是限制你的创意,而是给你提供基础保障,让你不用每次做菜都从"如何握刀"开始摸索。
设计模式也是一样的道理。它们是面