ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

设计模式复习指南:从七大原则到23种模式的选型与避坑

设计模式复习指南:从七大原则到23种模式的选型与避坑

简介:这份《大话设计模式复习》文档面向具备一定面向对象编程基础、工作1至3年的研发人员,系统梳理设计模式的核心知识体系。内容从设计模式的定义、分类、特点与应用场景切入,详细讲解单一职责、开放-封闭、依赖倒转、里氏代换、迪米特法则、合成/聚合及接口隔离七大设计原则,并深入剖析抽象工厂、策略、外观、适配器、组合、单例、中介者七种常用模式,每种模式均配以定义、UML类图、基础代码与实例说明,同时覆盖其余十五种模式,最后对比模式差异、总结选择维度,并附练习题与模拟试卷辅助巩固。资源包为1个docx文档,大小约3.66MB,目录结构清晰,便于按章节检索学习。目前已有191人学习。读者可借此掌握模式适用场景与实现细节,提升代码可维护性、可扩展性与复用性,并在实际项目中更准确地选择合适的设计模式。

1. 设计模式:为什么背了 23 个模式,代码里还是一个 if-else 走天下

很多人学设计模式的方式是背定义:单例是"保证一个类仅有一个实例",策略是"定义一系列算法并使其可互换"。背完之后打开项目,发现业务代码里依然是一坨 if-else,新增一个折扣类型就改一次主流程,改完还要回归测试所有分支。问题不在于模式没背熟,而在于没人讲清楚模式到底在解决什么结构性问题。

设计模式的本质不是代码模板,而是对"变化点"的封装约定。每当你发现某段逻辑会因为需求变更而反复修改,就应该把它抽出来,用一套固定的协作结构隔离它。这份复习资料的价值在于,它把 23 种模式按创建型、结构型、行为型三类拆开,每种模式都配了 UML 类图、基础代码和真实场景实例,并且专门做了易混淆模式的对比表。它回答的不是"这个模式是什么",而是"这个模式在什么条件下比另一个更合适"。

2. 七大设计原则:模式背后的约束条件

2.1 为什么先讲原则再讲模式

设计模式是"术",设计原则是"道"。如果跳过原则直接看模式,你会觉得每个模式都像是一种编码风格偏好,学完不知道什么时候该用哪个。但把七大原则放在前面,逻辑就通了:每个模式都是为了满足一条或多条原则而存在的。

单一职责原则(SRP)说的是一个类只负责一件事。这条原则直接解释了为什么策略模式要把每个算法拆成独立的类,而不是写在一个方法里用 switch 分支。开放-封闭原则(OCP)说的是对扩展开放、对修改关闭。抽象工厂模式之所以要定义抽象工厂接口,就是为了在新增产品族时只加新类、不改旧代码。依赖倒转原则(DIP)说的是高层模块和低层模块都依赖抽象。工厂方法模式里客户端只持有抽象产品接口,具体产品类怎么换都不影响调用方。

里氏代换原则(LSP)容易被忽略但很关键:子类必须能替换父类而不破坏行为契约。这条原则在策略模式里体现得最明显——每个具体策略类都实现了同一个接口,上下文类不关心用的是哪个策略,因为它们的契约一致。迪米特法则(LOD)说的是不要和陌生人说话,一个对象尽量少了解其他对象的内部结构。中介者模式就是这条原则的极端应用:同事类之间完全不直接通信,所有交互都通过中介者转发。

合成/聚合原则(CARP)说的是优先用组合而不是继承。装饰模式是这条原则的典型:你不需要为每种功能组合创建一个子类,而是把装饰器一层层套上去。接口隔离原则(ISP)说的是接口要小而专,不要搞一个大而全的接口让所有实现类都去实现一堆用不到的方法。

2.2 原则之间的冲突怎么处理

这七条原则不是孤立的,它们之间经常打架。比如你为了满足 SRP 把类拆得很细,结果类数量爆炸,类之间的依赖关系变得复杂,反而违反了迪米特法则。你为了满足 OCP 大量使用抽象和继承,结果继承层次太深,又违反了合成/聚合原则。

常见的处理思路是:优先保证 OCP 和 DIP,因为这两条直接决定了系统的可扩展性;SRP 和 ISP 在类设计层面指导拆分粒度;LSP 是正确使用继承的前提,如果做不到 LSP 就不要用继承;LOD 和 CARP 在模块间交互层面起作用,用来控制耦合度。实际项目中不需要每条都做到极致,关键是知道自己在哪个维度上做了妥协,以及为什么妥协。

注意:原则是判断依据,不是评分标准。一段代码违反了某条原则不一定是错的,但如果它同时违反了三条以上,大概率会在下一次需求变更时出问题。

3. 抽象工厂与策略模式:两个最常被误用的模式

3.1 抽象工厂到底在"抽象"什么

抽象工厂模式的定义是:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们的具体类。这句话的关键词是"一系列"和"相互依赖"。它不是创建单个对象,而是创建一组必须搭配使用的对象。

资料里给的数据库访问实例很典型:IFactory接口声明了CreateUser()和CreateDepartment()两个方法,SqlServerFactory和AccessFactory分别实现它们,各自返回对应数据库的具体 User 和 Department 对象。客户端代码只持有IFactory引用,运行时决定用哪个工厂,整个数据访问层的实现就切换了。

// 抽象工厂接口:声明创建产品族的方法 public interface IFactory { IUser CreateUser(); IDepartment CreateDepartment(); } // 具体工厂:SqlServer 实现 public class SqlServerFactory implements IFactory { public IUser CreateUser() { return new SqlserverUser(); // 返回 SqlServer 专用的 User 实现 } public IDepartment CreateDepartment() { return new SqlserverDepartment(); // 返回 SqlServer 专用的 Department 实现 } } // 客户端:只依赖抽象接口,不依赖具体实现 IFactory factory = new SqlServerFactory(); IUser iu = factory.CreateUser(); iu.Insert(user); iu.GetUser(1);

这段代码里,IFactory是抽象工厂,IUser和IDepartment是抽象产品,SqlserverUser和SqlserverDepartment是具体产品。客户端从头到尾没有出现过new SqlserverUser()这样的代码,所有对象的创建都委托给了工厂。这就是依赖倒转原则的落地:客户端依赖IUser接口,而不是SqlserverUser类。

抽象工厂的代价也很明确:如果要新增一个产品等级结构(比如再加一个IScore接口),就需要修改IFactory接口以及所有具体工厂类。这违反了开放-封闭原则。所以抽象工厂适合产品族稳定、但产品族内部实现经常切换的场景,比如多数据库支持、多主题 UI 渲染。

3.2 策略模式为什么不是简单的 if-else 替换

策略模式的定义是:定义一系列算法,将每个算法封装到独立的类中,使它们可以互换。很多人第一次看到策略模式会觉得这不就是把 if-else 拆成了类吗?确实,从行为上看,策略模式和 if-else 分支做的是同一件事——根据条件选择不同的处理逻辑。但区别在于结构。

if-else 的问题不在于它本身,而在于它把"选择逻辑"和"执行逻辑"耦合在了同一个方法里。每次新增一个分支,你都要修改这个方法,重新测试所有分支。策略模式把每个算法的实现独立出去,上下文类只负责持有策略引用和调用策略方法,新增算法只需要加一个新类,上下文类完全不动。

// 策略接口:所有算法的公共契约 interface Strategy { void doAlgorithm(); } // 具体策略 A class StrategyA implements Strategy { public void doAlgorithm() { /* 算法A的实现 */ } } // 上下文:持有策略引用,委托执行 class Context { private Strategy strategy; public void setStrategy(Strategy s) { strategy = s; } public void executeStrategy() { strategy.doAlgorithm(); } }

资料里的商场收银系统实例把这个结构落到了具体业务上:正常收费、满减、打折三种计价方式各自是一个策略类,收银台是上下文。新增一种促销方式时,只需要实现一个新的策略类,收银台的代码不需要任何改动。

策略模式的适用条件是:算法经常变化,且不同算法之间可以互相替换。如果算法很少变化,或者算法之间差异太大无法抽象出统一接口,硬套策略模式反而会增加类的数量和维护成本。

3.3 抽象工厂和工厂方法的关键区别

资料里专门做了对比:抽象工厂是为创建一组相关对象提供接口,工厂方法是为创建一类对象提供接口。换句话说,工厂方法只有一个产品等级结构,抽象工厂有多个。如果你只需要创建一种类型的对象,用工厂方法就够了;如果你需要创建的对象必须搭配使用(比如 SqlServer 的 User 必须配 SqlServer 的 Department),那就需要抽象工厂。

4. 结构型模式:适配器、组合、外观的协作机制

4.1 适配器模式解决的是什么问题

适配器模式的核心场景是:你有一个已有的类,它的接口和当前系统需要的接口不匹配,但你不想(或者不能)修改这个已有类的代码。适配器在中间做一层转换,把已有类的接口转换成目标接口。

资料里把适配器和装饰器、代理放在一起对比,这三者结构相似但意图完全不同。适配器是"改变接口",装饰器是"增强功能但不改变接口",代理是"控制访问但不改变接口"。判断标准很简单:调用方看到的接口变了吗?变了就是适配器,没变就是装饰器或代理。

4.2 组合模式与装饰模式的边界

组合模式把对象组织成树形结构,让客户端对单个对象和组合对象的使用具有一致性。装饰模式通过组合的方式给对象动态添加职责。两者都用了组合关系,但目的不同:组合模式关注的是"部分-整体"的层次结构,装饰模式关注的是"功能叠加"。

资料里的对比表指出:组合模式的典型场景是文件系统、UI 控件树这类树形结构;装饰模式的典型场景是 I/O 流处理、中间件管道这类需要层层包装的场景。如果你发现自己在写一个递归遍历的树形结构,考虑组合模式;如果你发现自己在写一堆可选的增强逻辑,考虑装饰模式。

4.3 外观模式的定位

外观模式为子系统提供统一的入口,简化客户端调用。它不改变子系统的功能,只是把多个子系统的调用编排成一个高层接口。资料里把外观模式归入七大重点模式之一,原因是它在实际项目中出现频率极高——任何分层架构的 Service 层本质上都是外观模式的应用。

外观模式和中介者模式容易混淆。外观模式是单向的:客户端通过外观调用子系统,子系统不知道外观的存在。中介者模式是双向的:同事类之间通过中介者通信,中介者知道所有同事类,同事类也知道中介者。简单说,外观是"简化调用",中介者是"解耦交互"。

5. 避坑:学设计模式时最容易翻车的几个地方

5.1 把模式当银弹,到处硬套

现象:学完策略模式后,把项目里所有 if-else 都改成了策略类,结果类文件从 20 个涨到 80 个,改一个需求要跳转五六个文件才能看懂完整逻辑。

原因:策略模式适合算法经常变化且需要运行时切换的场景。如果分支逻辑很少变动,或者分支之间共享大量上下文状态,拆成独立类反而增加了理解成本。

解决:先用"变化频率"和"变化独立性"两个维度判断。变化频率低、变化之间有关联的逻辑,保留在原方法里更合适。只有当某个分支确实会独立演化时,才值得抽成策略类。

5.2 抽象工厂新增产品等级时改到崩溃

现象:用抽象工厂做了多数据库支持,后来要加一个 Redis 缓存层,发现IFactory接口要加方法,所有具体工厂类都要改,改完还要逐个测试。

原因:抽象工厂的固有缺陷——新增产品等级结构需要修改抽象工厂接口,违反开放-封闭原则。这不是用错了,而是抽象工厂本身的取舍。

解决:如果产品等级结构可能频繁扩展,考虑用工厂方法替代抽象工厂,或者把工厂接口拆成多个小接口(接口隔离原则)。如果产品族稳定但产品等级会增长,可以在抽象工厂之上再加一层简单工厂来管理工厂的创建。

5.3 单例模式在多线程下创建了多个实例

现象:单例类的构造函数里加了日志,发现启动时打印了两次"创建实例"。

原因:经典的懒汉式单例没有加同步控制,两个线程同时判断instance == null为真,各自创建了一个实例。

解决:用双重检查锁定(DCL)加volatile关键字,或者直接用静态内部类/枚举实现。资料里虽然把单例归入剩余模式分析,但单例的线程安全问题在实际面试和代码审查中出现频率极高,值得单独花时间搞透。

5.4 混淆策略模式和状态模式

现象:用策略模式实现了一个订单状态流转逻辑,结果发现状态之间的转换条件散落在各个策略类里,维护起来比 if-else 还乱。

原因:策略模式的策略之间是平等的、可互换的,客户端主动选择策略。状态模式的状态之间有转换关系,状态自己决定下一个状态是什么。用策略模式实现状态机,等于把状态转换逻辑从集中式变成了分布式。

解决:如果对象的行为取决于它的状态,且状态之间有明确的转换规则,用状态模式。如果只是运行时选择不同的算法,算法之间没有转换关系,用策略模式。

5.5 观察者和中介者分不清

现象:实现了一个事件通知系统,一开始用观察者模式,后来发现观察者和被观察者之间的依赖关系太复杂,改成了中介者模式,结果中介者类膨胀成了一个上帝类。

原因:观察者模式是"一对多"的广播关系,被观察者直接持有观察者列表。中介者模式是"多对多"的星型关系,所有同事类只和中介者通信。观察者适合事件通知,中介者适合复杂交互的解耦。

解决:如果对象之间的交互关系是简单的发布-订阅,用观察者。如果对象之间的交互关系是多对多且经常变化,用中介者,但要注意控制中介者的职责范围,避免它变成什么都管的上帝类。

6. 模式选择的判断框架:从场景反推模式

6.1 用变化维度做第一层筛选

选择模式的第一步不是看模式定义,而是识别当前代码里的"变化点"在哪里。变化点决定了你需要哪种类型的模式。

如果变化点在"对象怎么创建",考虑创建型模式。对象创建逻辑复杂、需要根据条件选择不同实现,用工厂方法或抽象工厂;对象创建成本高需要复用,用原型模式;对象构建步骤多且顺序固定,用建造者模式;全局只需要一个实例,用单例模式。

如果变化点在"对象怎么组合",考虑结构型模式。接口不兼容需要转换,用适配器;需要动态添加功能,用装饰器;需要简化子系统调用,用外观;需要树形结构统一处理,用组合;需要控制对象访问,用代理;需要共享细粒度对象,用享元;需要分离抽象和实现,用桥接。

如果变化点在"对象怎么交互",考虑行为型模式。算法需要互换,用策略;状态需要流转,用状态;一对多通知,用观察者;多对多解耦,用中介者;请求需要沿链传递,用责任链;操作需要撤销,用命令加备忘录;算法骨架固定但步骤可变,用模板方法;数据结构需要遍历,用迭代器;需要对对象结构做操作,用访问者。

6.2 用对比表做第二层确认

资料里整理了多组易混淆模式的对比,这些对比表是模式选择时最有价值的参考。比如策略模式 vs 状态模式:策略模式的客户端知道所有策略并主动选择,状态模式的状态转换对客户端透明。观察者模式 vs 中介者模式:观察者是单向通知,中介者是双向协调。适配器 vs 装饰器 vs 代理:适配器改变接口,装饰器增强功能,代理控制访问。

这些对比的本质是:结构相似的模型,意图不同,适用场景就不同。判断时不要看代码结构,要看"谁在什么时候决定调用哪个类"以及"调用方对被调用方的了解程度"。

6.3 一个实际项目的模式组合案例

资料里的可移植数据访问程序同时用到了抽象工厂、单例和外观。IFactory是抽象工厂,负责创建 User 和 Department 对象族;具体工厂类可以用单例模式保证全局只有一个实例;数据访问层对外暴露的 Service 接口是外观模式,隐藏了工厂创建和对象操作的细节。

这个组合的逻辑是:外观模式提供统一入口,抽象工厂负责对象创建的解耦,单例控制工厂实例的数量。三个模式各司其职,没有重叠。如果你在项目中发现自己用了一个模式就能解决所有问题,大概率是模式用错了;如果用了一堆模式但说不清每个模式解决了什么变化点,大概率是过度设计。

6.4 验证模式是否用对的三个问题

第一个问题:如果新增一种同类需求,我需要改几个文件?用对了模式,答案通常是"只加一个新类,不改旧代码"。如果需要改接口或改调用方,说明抽象层次没找对。

第二个问题:如果删掉这个模式,代码会变成什么样?如果删掉之后只是多了一个 if-else 分支,说明这个模式带来的收益有限。如果删掉之后需要修改多个调用方、引入大量条件判断,说明这个模式确实隔离了变化。

第三个问题:团队里其他人能不能在不看文档的情况下理解这个模式的使用意图?如果类名和接口名足够清晰(比如CashNormalStrategy、CashReturnStrategy),说明模式用对了。如果类名是HandlerA、HandlerB,说明抽象没做到位。

我自己的习惯是:每次引入一个模式之前,先写一段最朴素的实现,然后列出未来可能的变化点,再判断这个模式能不能让变化点的影响范围最小化。如果答案是否定的,就不引入。从那以后我每次做模式选型都强制走一遍"变化点识别 → 模式匹配 → 对比确认 → 影响范围验证"这个流程,踩坑次数明显少了。希望帮到你。

这份复习资料把 23 种模式的定义、UML 类图、基础代码、实例和对比表都整理在了一起,适合在选型时快速翻阅对照。需要完整文档的话,可以顺着这份资料的目录结构去获取。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进