ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

设计模式选型与落地:七大原则、高频模式实现及避坑指南

设计模式选型与落地:七大原则、高频模式实现及避坑指南

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

1. 从一份复习文档到可交付的设计模式方案:为什么模式选型总在项目里翻车

很多同学做软件工程课设或毕设时,功能跑通了,代码却越写越乱:加一个折扣策略要改五六个文件,换一个数据库实现要动客户端代码,对象创建散落各处。问题不在语法,而在设计模式没有落到方案层面。这份《大话设计模式复习》文档覆盖了 23 种经典模式、七大设计原则、模式对比和模拟试卷,正好能当作一套完整的复习与落地参考。它适合两类人:一是正在准备软件工程考试或课程设计的学生,需要把抽象工厂、策略、外观、适配器、组合、单例、中介者这七种高频模式吃透;二是工作 1 到 3 年、想系统梳理面向对象设计经验的开发者。下面按需求、设计、实现、避坑、验收的顺序,把这份资料拆成一份可以直接照着走的方案。

2. 需求与设计原则:七大原则怎么约束模式选型

2.1 从需求场景反推原则优先级

拿到一个课设或模块开发任务,第一步不是翻模式目录,而是把需求里的变化点标出来。比如商场收银系统,变化点在折扣算法;可移植数据访问程序,变化点在数据库类型。变化点确定后,再对照七大设计原则做取舍。

单一职责原则要求一个类只有一个引起变化的原因。收银系统里,正常收费、打折、满减各自独立成类,而不是塞进一个 CashSuper 里写 if-else。开放-封闭原则要求对扩展开放、对修改关闭,策略模式新增一种折扣只需加一个具体策略类,不改上下文。依赖倒转原则要求高层模块和低层模块都依赖抽象,工厂模式里客户端依赖 IUser 接口而不是 SqlserverUser 具体类。里氏代换原则要求子类能替换父类且行为一致,正方形继承矩形时面积计算逻辑不能变。迪米特法则要求减少直接交互,中介者模式让同事类通过中介者通信。合成/聚合原则优先用组合而非继承,装饰模式就是典型。接口隔离原则要求接口细化,把动物接口拆成飞行动物和陆地动物。

这七条原则不是并列关系,实际项目里优先级通常是:先满足单一职责和开放-封闭,再用依赖倒转解耦,最后用迪米特和合成/聚合降低类间依赖。接口隔离和里氏代换更多是检查项,用来验证设计有没有明显违规。

2.2 用 UML 类图把原则翻译成结构

文档里反复出现 UML 类图,抽象类用斜体表示,类之间有关系连线。这一步很关键,因为原则是文字,类图是结构。以抽象工厂为例,类图里有四层:AbstractFactory 声明 createProductA 和 createProductB,ConcreteFactory1 和 ConcreteFactory2 分别实现,AbstractProductA 和 AbstractProductB 定义产品接口,ProductA1、ProductA2、ProductB1、ProductB2 是具体产品。

画类图时我一般会检查三件事:抽象层是否稳定,具体层是否可替换,客户端是否只依赖抽象。如果客户端代码里出现了 new SqlserverUser() 这样的具体类名,依赖倒转就没做到。如果新增一个产品族要改 AbstractFactory 接口,说明开放-封闭在抽象工厂这个方向上是有代价的,文档里也明确写了这一点:增加新产品的等级结构需修改抽象工厂和所有具体工厂,违反开放-封闭原则。这不是设计错误,而是抽象工厂的固有取舍,选型时要提前知道。

2.3 原则与模式的映射关系

把七大原则和 23 种模式做一张映射表,复习和答辩时都能用。

设计原则主要约束的模式检查点
单一职责策略、命令、状态每个具体类是否只有一个变化原因
开放-封闭策略、工厂方法、装饰新增行为是否只加类不改原有类
依赖倒转抽象工厂、工厂方法、桥接客户端是否依赖抽象接口
里氏代换组合、模板方法子类替换父类后行为是否一致
迪米特中介者、外观类之间是否通过中间层通信
合成/聚合装饰、组合、桥接是否用组合替代了继承扩展
接口隔离适配器、外观接口是否被拆成职责单一的小接口

这张表的作用是反向验证:如果某个模式用完后发现违反了多条原则,要么是场景不匹配,要么是实现方式有问题。比如用单例模式管理全局配置,如果配置类同时承担了读取、解析、缓存三个职责,就违反了单一职责,应该拆开。

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(); } public IDepartment CreateDepartment() { return new SqlserverDepartment(); } } // 客户端:只依赖抽象工厂和抽象产品 IFactory factory = new SqlServerFactory(); IUser iu = factory.CreateUser(); iu.Insert(user); iu.GetUser(1);

这段代码的关键在于客户端没有出现任何具体产品类名。IFactory 是抽象工厂,IUser 和 IDepartment 是抽象产品,SqlserverUser 和 SqlserverDepartment 是具体产品。参数说明:CreateUser 返回 IUser 接口,Insert 接收 User 对象,GetUser 接收 int 类型 id。如果要新增一个 Oracle 产品族,只需实现 OracleFactory、OracleUser、OracleDepartment,客户端改一行工厂实例化即可。但如果是新增一种产品类型,比如 ILogin,就需要修改 IFactory 接口和所有具体工厂,这是抽象工厂的代价。

策略模式解决的是算法族切换问题。文档里的商场收银系统是标准案例:CashSuper 是抽象策略,CashNormal、CashReturn、CashRebate 是具体策略,Context 持有 CashSuper 引用并委托执行。

// 策略接口 public abstract class CashSuper { public abstract double acceptCash(double money); } // 具体策略:满减 public class CashReturn extends CashSuper { private double moneyCondition = 0; private double moneyReturn = 0; public CashReturn(double moneyCondition, double moneyReturn) { this.moneyCondition = moneyCondition; this.moneyReturn = moneyReturn; } public double acceptCash(double money) { if (money >= moneyCondition) { return money - Math.floor(money / moneyCondition) * moneyReturn; } return money; } } // 上下文:持有策略并委托执行 public class CashContext { private CashSuper cs; public CashContext(CashSuper cs) { this.cs = cs; } public double getResult(double money) { return cs.acceptCash(money); } }

参数说明:moneyCondition 是满减条件,moneyReturn 是减免金额,acceptCash 接收原始金额返回折后金额。策略模式的核心是把算法从客户端剥离,客户端只和 Context 交互。和简单工厂结合时,可以在 Context 构造函数里根据字符串创建具体策略,但这样 Context 就承担了创建职责,需要权衡是否违反单一职责。

3.2 外观、适配器与组合模式:结构型模式的三种解耦方式

外观模式为子系统提供统一入口。文档里的外观模式适合这样的场景:一个模块内部有多个类协作完成功能,外部调用者不需要知道内部细节。实现方式是创建一个 Facade 类,持有子系统对象的引用,对外暴露一个简化方法。

// 子系统类 public class SubSystemOne { public void methodOne() { System.out.println("子系统方法一"); } } public class SubSystemTwo { public void methodTwo() { System.out.println("子系统方法二"); } } // 外观类:组合子系统,对外提供统一接口 public class Facade { private SubSystemOne one; private SubSystemTwo two; public Facade() { one = new SubSystemOne(); two = new SubSystemTwo(); } public void methodA() { one.methodOne(); two.methodTwo(); } }

外观模式的关键是 Facade 不增加新功能,只做委托和组合。如果 Facade 里出现了业务判断逻辑,说明职责越界了。适配器模式解决接口不兼容问题,文档里的适配器模式通过继承或组合把 Adaptee 的接口转换成 Target 接口。组合模式处理树形结构,文档里的组合模式让叶子节点和容器节点实现同一接口,客户端可以一致地处理单个对象和组合对象。

这三种结构型模式的共同点是都在做解耦,但方向不同:外观是简化对外接口,适配器是转换接口,组合是统一接口。选型时先判断问题是接口太多、接口不匹配还是结构是树形,再对应选择。

3.3 单例与中介者:全局唯一与多对多解耦

单例模式保证一个类只有一个实例。文档里的单例模式基础实现是私有构造函数加静态方法返回唯一实例。多线程环境下需要加锁或使用静态内部类。

public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }

参数说明:synchronized 保证线程安全,但每次调用都加锁影响性能。更常用的做法是双重检查锁定或静态内部类。单例的坑在于:如果单例持有可变状态,测试时很难隔离;如果单例初始化依赖外部资源,启动顺序不好控制。我一般只在配置管理、连接池、日志器这类确实需要全局唯一的场景用单例。

中介者模式解决同事类之间多对多通信问题。文档里的中介者模式让每个同事类持有 Mediator 引用,通信时通过中介者转发,而不是直接调用其他同事类。

// 抽象中介者 public abstract class Mediator { public abstract void send(String message, Colleague colleague); } // 抽象同事类 public abstract class Colleague { protected Mediator mediator; public Colleague(Mediator mediator) { this.mediator = mediator; } } // 具体同事类 public class ConcreteColleagueA extends Colleague { public ConcreteColleagueA(Mediator mediator) { super(mediator); } public void send(String message) { mediator.send(message, this); } public void notify(String message) { System.out.println("同事A收到消息:" + message); } }

中介者的代价是中介者本身可能变得复杂。如果同事类数量多、交互逻辑复杂,中介者会膨胀成上帝类。通常的做法是:交互规则简单时用中介者,规则复杂时考虑拆成多个中介者或改用事件总线。

4. 模式对比与选型:15 种剩余模式的归类方法

4.1 创建型模式对比:简单工厂、工厂方法与抽象工厂

文档里给出了创建型模式的对比维度。简单工厂是一个工厂类根据参数创建不同产品,工厂方法是每个产品对应一个工厂类,抽象工厂是一个工厂类创建一组产品族。

对比项简单工厂工厂方法抽象工厂
产品数量单一产品单一产品产品族
扩展方式改工厂类加工厂类加工厂类
是否违反开放-封闭是否增加产品族时是
适用场景产品少且稳定产品扩展频繁多产品族切换

选型逻辑:如果产品种类少且不常变,简单工厂够用;如果产品种类会持续增加,用工厂方法;如果需要整体切换产品族,比如从 SqlServer 切到 Access,用抽象工厂。建造者模式关注的是分步构建复杂对象,和工厂模式的区别在于工厂返回产品,建造者返回构建过程。

4.2 结构型与行为型模式对比:易混淆模式辨析

文档里列出了几组容易混淆的模式。适配器、装饰器、代理模式都涉及包装对象,但意图不同:适配器改变接口,装饰器增强功能,代理控制访问。组合模式和装饰器模式都涉及递归组合,但组合模式处理树形结构,装饰器模式处理功能叠加。

行为型模式里,策略模式和状态模式结构相似,但策略模式由客户端选择算法,状态模式由对象状态自动切换行为。观察者模式和中介者模式都涉及对象间通信,观察者是一对多通知,中介者是集中式转发。责任链模式和中介者模式都解耦了发送者和接收者,责任链是链式传递,中介者是星型转发。

4.3 模式选择的关键维度

文档最后总结了模式选择的关键维度。我一般按四个问题来筛:第一,变化点在哪里,是对象创建、接口不兼容、算法切换还是状态转换;第二,变化频率如何,频繁变化用模式封装,稳定不变直接写;第三,类数量是否可控,模式会增加类数量,小项目过度使用会降低可读性;第四,团队是否熟悉该模式,不熟悉的模式会增加沟通成本。

这四个问题回答完,模式基本就确定了。如果两个模式都符合,选类数量少、团队更熟悉的那个。模式是手段不是目的,代码可维护性才是目标。

5. 避坑与常见问题:模式使用中最容易翻车的五个点

5.1 抽象工厂新增产品类型导致全量修改

现象:项目初期用抽象工厂管理数据库访问,后来要新增一个登录日志产品,发现要改 IFactory 接口、SqlServerFactory、AccessFactory 以及所有相关类。

原因:抽象工厂的接口是按产品族设计的,新增产品类型意味着所有具体工厂都要实现新方法,这违反了开放-封闭原则。

解决:如果产品类型会持续增加,不要用抽象工厂,改用工厂方法或简单工厂加配置。如果只是偶尔新增,接受这次修改,但要在设计文档里记录这个取舍。

5.2 策略模式与简单工厂混用导致 Context 职责过重

现象:CashContext 构造函数里根据字符串创建具体策略,后来策略种类增加到十几种,Context 里堆满了 if-else。

原因:策略模式负责算法切换,简单工厂负责对象创建,两者混在一个类里,Context 同时承担了创建和使用职责。

解决:把创建逻辑抽到独立的工厂类,Context 只接收策略对象。如果客户端需要根据配置选择策略,可以在工厂类里做映射,Context 保持单一职责。

5.3 单例模式在测试中无法隔离

现象:单元测试时,前一个测试修改了单例的状态,后一个测试读到脏数据,测试结果不稳定。

原因:单例的生命周期和进程一致,测试之间共享同一个实例。

解决:单例类提供 reset 方法用于测试,或者用依赖注入替代单例,把全局实例改为可注入的普通对象。如果必须用单例,测试时通过反射重置静态字段。

5.4 中介者膨胀为上帝类

现象:中介者模式用了一段时间后,Mediator 类里出现了大量条件判断和业务逻辑,修改一个同事类的交互规则要改中介者。

原因:所有同事类的通信都经过中介者,交互规则复杂时中介者自然膨胀。

解决:按业务域拆分多个中介者,或者引入事件机制替代直接转发。中介者只负责路由,不负责业务判断。

5.5 过度使用模式导致类数量爆炸

现象:一个小模块用了五六个模式,类文件几十个,新人接手时找不到入口。

原因:为了用模式而用模式,忽略了项目规模和团队熟悉度。

解决:小项目优先用简单工厂和策略,结构型模式按需引入。每引入一个模式,问自己:不用这个模式,代码会差在哪里?如果答案不明确,就不引入。

6. 验收与答辩:怎么证明你的模式选型是对的

验收设计模式方案,不是看用了多少种模式,而是看变化点是否被封装、客户端是否依赖抽象、新增功能是否只加类不改原有代码。我一般用三个检查动作来验收。

第一个动作:画变化点映射表。把需求里所有可能变化的点列出来,每个变化点对应一个模式或一个设计决策。比如折扣算法变化对应策略模式,数据库类型变化对应抽象工厂,界面调用简化对应外观模式。答辩时这张表能直接说明每个模式的存在理由。

第二个动作:做扩展演练。假设要新增一种折扣、新增一个数据库、新增一个子系统方法,看需要改哪些文件。如果只加新类不改原有类,开放-封闭就做到了。如果必须改接口或工厂类,说明这个方向的扩展是有代价的,要能解释为什么接受这个代价。

第三个动作:检查客户端代码。客户端里不应该出现具体产品类名、具体策略类名、具体子系统类名。如果出现了,依赖倒转就没做到。客户端只应该依赖抽象接口和上下文类。

验收项合格标准检查方式
变化点封装每个变化点有对应模式或设计决策变化点映射表
开放-封闭新增功能只加类不改原有类扩展演练
依赖倒转客户端不出现具体类名代码扫描
单一职责每个类只有一个变化原因类职责清单
模式数量合理每个模式都有明确的存在理由模式理由表

答辩时还有一个技巧:准备一个反例。说明如果不用这个模式,代码会变成什么样。比如不用策略模式,acceptCash 里会有大量 if-else;不用抽象工厂,客户端会直接 new SqlserverUser。反例比正例更能说明模式的价值。

模拟试卷里的题也值得过一遍。文档提供了题和卷子,做题时不要只记答案,要回到模式定义和 UML 类图去验证。选择题考的是模式分类和适用场景,简答题考的是模式对比和原则应用。把错题对应的模式重新画一遍类图,比背答案有效。

从那以后我每次做模式选型,都强制走一遍变化点映射和扩展演练,确认每个模式都有不可替代的理由才写进方案。希望帮到你。

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

RELATED READING

延伸阅读

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