简介:王桂林《设计模式》PDF是一份面向软件工程师与在校学生的设计模式入门讲义,内容从模式语言的历史讲起,系统说明如何用模式名称、问题、解决方案、效果与结构图来陈述模式,并给出学习建议与经典书目推荐。资源重点剖析单一职责原则、开闭原则等基础设计原则,强调通过分解类、依赖接口等方式提高代码的可扩展性与可维护性,帮助读者建立面向对象设计的规范思维。包内共1个PDF文件,压缩包大小约3.09MB,内容层次清晰,先从概念和原则切入,再逐步深入具体模式,适合零基础读者系统阅读,也可作为日常开发中的速查手册。目前已有157人学习浏览,适合希望提升代码质量、促进团队协作并加快开发进度的开发者下载学习。
1. 一份《设计模式》PDF,为什么值得你从头读一遍
设计模式这个话题,从业者基本都听过,但真正能把二十多个模式用对场合的人不多。我见过不少同事把PDF下载到本地,翻到单例就划走,或者照着代码抄一遍,结果项目里全是 new 对象,模式没带来任何收益。今天这篇笔记,源头就是一份流传较广的《设计模式》PDF——它把经典著作里的概念压缩成了几十页,保留了模式对比、类图和适用场景。这份材料最大的价值不是教你背定义,而是帮你建立“看到坏味道→想起模式→重构落地”的反应链。适合刚学完面向对象但写代码总被改需求折腾的人,也适合带徒弟时用来做训练素材。下面我按自己的实践经验,把这类设计模式资料的用法拆成五步:先懂原理,再跑通最小示例,接着在真实项目里找切入点,最后避开那些我踩过的坑。
2. 设计模式的底层逻辑:先弄清它解决的是什么问题
很多人在 PDF 里看到一个模式的类图,第一反应是“这些类怎么画出来的”,然后照着画一遍,以为会画图就是会用了。其实设计模式的本质是对“变化”的预判和封装。没有变化,就没有模式存在的必要。一个需求写死、永远不会改的系统,用 if-else 和直接 new 对象最简单。模式引入的抽象、接口、间接层,都是为了将来某个方向的变化而付出成本。所以学模式之前,必须先建立两个意识:第一,模式是权衡的结果,不是越多越好;第二,模式的选择依据是“变化点在哪里”,而不是“这个类像不像某个图”。
2.1 设计模式是对“变化点”的封装,不是固定套路
理解模式的关键,是区分“稳定部分”和“变化部分”。比如策略模式,稳定的是“客户端调用算法”的入口,变化的是“具体算法实现”;观察者模式,稳定的是“事件发布与订阅”的框架,变化的是“订阅者列表和通知顺序”。模式做的事情,就是把变化的部分抽出来,让它可以在运行时独立替换,而不影响稳定部分。这一点在 GoF 的原始定义里反复强调:“封装变化”才是模式的灵魂。
我在实际项目里判断该不该引入某个模式,会先问自己:这段代码未来可能因为什么原因改变?如果答案是“不止一个方向”,那模式值得考虑;如果答案是“根本不会变”,那直接写死反而更好。举个例子,一个基础数据转换工具,目前只转换 JSON 到对象,以后可能要支持 XML、YAML,那转换逻辑就是变化点,可以用策略模式或工厂模式把它包一层。但如果你的项目只在内存里跑,根本不会引入其他格式,硬套模式就是给自己找麻烦。
2.2 读懂一个模式必须抓住的四要素:意图、结构、参与者、后果
拿到 PDF 里任何一张模式图,不要急着背类名。我通常按四个维度拆解,这个习惯帮我避免了大量“看着像但用不对”的情况。第一个是意图,也就是这个模式到底解决什么具体问题。第二个是结构,这里的结构不是类图本身,而是“谁依赖谁、谁拥有谁”。第三个是参与者,也就是模式里每个角色的职责边界。第四个是后果,包括好处和代价,这部分最容易被人忽略。
以工厂模式为例,意图是“让对象的创建和使用分离”,结构上把创建逻辑收拢到一个工厂类里,参与者包括产品接口、具体产品、工厂类,后果是调用方不再依赖具体类,但代价是增加了一个工厂类,如果产品只有两三个,这个间接层反而多余。PDF 里如果某个模式只画了图没写后果,我会用下面这个表来补全:
| 模式 | 意图(一句话) | 主要角色 | 典型代价 |
|---|---|---|---|
| 单例 | 保证全局唯一实例 | 单例类、调用方 | 隐藏全局状态,难测试 |
| 工厂 | 将创建逻辑从业务中剥离 | 产品接口、具体产品、工厂 | 类数量增加,调试链路变长 |
| 策略 | 让算法族可替换 | 上下文、策略接口、具体策略 | 策略类容易爆炸 |
| 观察者 | 一对多通知,解耦发布与订阅 | 主题、观察者、具体订阅者 | 通知顺序不可控,内存泄漏 |
| 模板方法 | 固定算法骨架,延迟步骤到子类 | 抽象类、具体实现 | 继承体系僵硬,组合受限 |
这四要素最大的作用,是把一个模式从“图片”还原成“决策记录”。你在 PDF 里看某个模式时,如果发现它的意图和你的场景对不上,那再漂亮的类图也和你无关。我见过最典型的情况是:有人看到装饰器模式能动态加功能,就把所有类都包一层装饰器,结果项目里出现 A 包装 B、B 包装 C 的十层嵌套,最后查日志都查不到具体实现,这就是没读“后果”的代价。
3. 把PDF变成肌肉记忆:三个最小可运行示例
光看 PDF 里的类和文字,很难真正掌握模式。我的习惯是把每个模式都写成能跑的最小代码,自己亲手跑一遍,再改动几个参数观察行为变化。这一步不需要搭复杂框架,一个 Java 文件、一个 main 方法就够。下面选三个最常用、也是面试最爱问的模式,给出最小可运行版本。代码以 Java 为例,因为设计模式经典示例大多是 Java 留传下来的,类型系统对角色边界表达得最清楚。
3.1 单例模式:饿汉、懒汉与线程安全的取舍
单例是最容易写错的一个模式。很多人一上来就写懒汉式,然后加 synchronized,结果性能又差又容易被反射破坏。我们先看一个最稳妥的饿汉式实现,它简单到不会出错,唯一的问题是类加载时就创建实例,如果这个实例创建很重且不一定用到,就会拖慢启动。
public class ConfigManager { // 饿汉式:类加载时创建,天然线程安全 private static final ConfigManager INSTANCE = new ConfigManager(); private String configPath; private ConfigManager() { // 私有构造,防止外部 new this.configPath = System.getenv("APP_CONFIG_PATH"); } public static ConfigManager getInstance() { return INSTANCE; } public String getConfigPath() { return configPath; } }这段代码的关键点是构造方法必须是 private,外部无法 new 第二个实例。INSTANCE被static final修饰,Java 保证在类加载阶段完成初始化,所以多线程并发调用getInstance()不会出现重复创建。如果你担心启动时开销,可以把System.getenv("APP_CONFIG_PATH")改成懒加载,用双重检查锁,但那个版本还要加volatile,否则对象引用可能半初始化。我在项目里一般默认先用饿汉式,除非实例创建依赖的配置在运行后才产生,否则没必要为了“可能不用”而牺牲简单性。
3.2 策略模式:用接口替换掉整段if-else
策略模式是最容易带来直接收益的模式。它的核心是定义一组算法接口,让每种算法成为独立类,上下文类持有接口引用,可以在运行时切换。下面是一个运费计算的例子,传统写法是 if 判断地区,然后各自打折,现在把每种运费规则变成一个策略。
interface FreightStrategy { double calculate(double weight, double distance); } // 策略一:普通快递 class NormalFreight implements FreightStrategy { @Override public double calculate(double weight, double distance) { return weight * 1.2 + distance * 0.5; } } // 策略二:同城急送 class CityExpressFreight implements FreightStrategy { @Override public double calculate(double weight, double distance) { return weight * 2.0 + distance * 1.0 + 5.0; } } // 上下文:订单结算时接收策略 class FreightCalculator { private FreightStrategy strategy; public FreightCalculator(FreightStrategy strategy) { this.strategy = strategy; } public void setStrategy(FreightStrategy strategy) { this.strategy = strategy; } public double calc(double weight, double distance) { return strategy.calculate(weight, distance); } }使用的时候,业务层只需要选择策略类,不再需要关心实现细节。这里的参数说明:weight和distance是业务原始数据,策略接口决定了所有算法都必须接收相同参数;如果你发现不同算法需要的参数不一样,说明接口抽象得有问题,得考虑用上下文对象封装参数。这个例子中NormalFreight和CityExpressFreight是两个策略类,新增一种物流方式时,只需新建一个类实现FreightStrategy,不需要改动FreightCalculator,这就是“开闭原则”的体现。我在真实项目里用这个模式替换掉了一个 300 行的 if-else 方法,替换后新增规则的时间从“改老代码加分支”变成“新写一个类”,风险小很多。
3.3 观察者模式:把“通知谁”从业务里拆出去
观察者模式解决的是“状态变化时,通知多个对象”的问题。后面我们会在避坑章节里讨论它容易踩的坑,先看一个正确的骨架。以订单状态变更为例,当订单支付成功后,需要通知库存系统、短信服务和积分系统。
import java.util.ArrayList; import java.util.List; // 观察者接口 interface OrderObserver { void onOrderPaid(Long orderId); } // 主题:订单状态 class OrderSubject { private List<OrderObserver> observers = new ArrayList<>(); private Long currentOrderId; public void attach(OrderObserver observer) { observers.add(observer); } public void detach(OrderObserver observer) { observers.remove(observer); } public void setPaid(Long orderId) { this.currentOrderId = orderId; // 状态变化后通知所有观察者 for (OrderObserver observer : observers) { observer.onOrderPaid(orderId); } } } // 具体观察者:库存服务 class InventoryObserver implements OrderObserver { @Override public void onOrderPaid(Long orderId) { System.out.println("库存扣减,订单: " + orderId); } } // 具体观察者:短信服务 class SmsObserver implements OrderObserver { @Override public void onOrderPaid(Long orderId) { System.out.println("发送订单通知短信: " + orderId); } }这段代码里,OrderSubject只负责维护观察者列表,并在状态变化时把通知发出去,它不关心观察者内部做什么。attach和detach用来动态增加减少订阅者。运行一个 main 方法,把InventoryObserver和SmsObserver加入主题,调用setPaid(1001L),两个服务都会收到通知。观察者模式的关键参数是“通知顺序”——上面的 for 循环按加入顺序通知,如果你有很多观察者,且逻辑上依赖某个观察者先执行,那这个模式就不再适用,需要引入事件总线或显式的优先级排序。这一点经常被忽略,也是我认为观察者模式最需要提前约定的一点。
4. 在真实项目里引入设计模式:推荐的两个切入点
学完最小示例,最常犯的错误是回到项目里到处套模式。我刚工作那两年就干过这事:看到一个类需要新的能力,立刻想到装饰器;看到创建对象的地方,立刻想套工厂。结果是项目里抽象满天飞,代码量翻了倍,可维护性反而下降。正确的做法是反着来的:先找到代码的坏味道,再从 PDF 里翻有没有对应模式。这一章我会给出我常用的判断顺序,以及两个几乎不会跑偏的切入点。
4.1 先找坏味道,再选模式,不要反着来
坏味道不是错误,而是已经让你不舒服的信号。最常见的三个信号是:重复代码、过长分支、参数列表膨胀。我通常先做静态扫描和人工排查,把坏味道列出来,然后对着设计模式分类表找候选方案。比如一个方法里有十几个 if-else 判断类型,那策略模式大概率能帮上忙;一个对象创建过程需要根据配置决定具体类,工厂模式很合适;两个类之间的依赖关系难以测试,可以看依赖倒置相关的模式,比如观察者或中介者。
有个实用的操作顺序:先画一张“变化方向”清单。拿一张纸,把模块里可能变的方向列出来,比如“新增计费方式”“新增数据源”“新增消息渠道”。然后对每个变化方向,问自己“当前代码需要改动哪些地方?”如果答案是“要改一个集中的地方”,那可能需要工厂;如果答案是“每个调用处都要改”,那是策略或模板方法该出场了;如果答案是“新功能需要往多个旧模块里塞代码”,观察者可以介入。这个顺序能避免你看到一个类像某个模式就冲动重构。
4.2 切入点一:用策略模式处理计价规则
计价、折扣、运费这类带大量规则的业务,是策略模式最好的试验田。我建议先从最脏的方法入手:找到那些带“type”参数并且内部用 switch 或 if-else 分支的方法。下面是一个重构前后的对比:
重构前,一个方法里塞了三种计价方式:
public double calcPrice(String userType, double amount) { if ("normal".equals(userType)) { return amount * 1.0; } else if ("vip".equals(userType)) { return amount * 0.9; } else if ("employee".equals(userType)) { return amount * 0.7; } return amount; }这段代码每加一个用户类型,就要改这个方法,而且方法会越来越长。重构后,把每种用户类型的折扣逻辑放到独立策略类里:
interface PriceStrategy { double calc(double amount); } class NormalPrice implements PriceStrategy { public double calc(double amount) { return amount; } } class VipPrice implements PriceStrategy { public double calc(double amount) { return amount * 0.9; } } class EmployeePrice implements PriceStrategy { public double calc(double amount) { return amount * 0.7; } }然后在订单服务里维护一个Map<String, PriceStrategy>,通过用户类型拿到对应策略。如果以后要加“企业客户”类型,只需要新增一个类,并往 Map 里注册一个键值,其他地方不需要改动。这里要注意一个参数问题:PriceStrategy的接口方法参数是double amount,如果不同策略需要的上下文不同,比如某个新策略还需要判断订单数、会员积分,接口就要扩展,建议使用一个PriceContext对象作为参数,而不是无脑传一堆基本类型。
4.3 切入点二:用工厂模式收敛对象创建
第二个我推荐切入的场景是对象创建逻辑散落各处。常见表现是:业务代码里频繁出现new XxxProcessor(type),而且创建之后还要做一堆初始化,如果创建逻辑分散,将来换实现时需要到处修改。工厂模式把创建逻辑收拢,业务调用方只依赖一个工厂方法。下面是一个消息处理器的例子:
interface MessageHandler { void handle(String message); } class SmsHandler implements MessageHandler { public void handle(String message) { System.out.println("send sms: " + message); } } class EmailHandler implements MessageHandler { public void handle(String message) { System.out.println("send email: " + message); } } class HandlerFactory { public static MessageHandler create(String channel) { switch (channel) { case "sms": return new SmsHandler(); case "email": return new EmailHandler(); default: return null; } } }调用方只跟HandlerFactory.create(channel)打交道,它自己不用知道SmsHandler和EmailHandler的存在。这样做的收益是,当你要把SmsHandler改用第三方 SDK 时,只需要修改工厂里的 new 那行;如果未来要支持“站内信”,也只改工厂。不过我要提醒你,工厂模式适合“创建逻辑稳定但实现会变”的场景,如果产品种类极少且永远不会增加,那工厂就是一层不必要的封装。判断标准很简单:你未来真的会频繁新增或替换实现吗?如果没有,别用。
5. 设计模式避坑:我踩过的5个坑与排查思路
这里写几个我真实经历过的踩坑记录,每条按“现象 → 原因 → 解决”展开。希望你看完能少走一段弯路。
5.1 坑一:为了“用上模式”而给一个不会变的类套上抽象
现象:某个团队接手老系统,发现所有接口都套了一层抽象接口,连一个只被实现一次的接口也存在,代码跳转好几层才能看到真实逻辑。
原因:开发者为了“可扩展”而预先抽象,但业务根本没有多种实现,也没有变化迹象,纯属为了模式而模式。
解决:每次抽出接口之前,先写一份“变化证据”:列出未来可能出现的第二个实现。写不出来,就先把接口去掉,直接用具体类。如果未来真出现了多个实现,再抽接口不晚,因为 IDE 的重构工具可以让这件事成本极低。
5.2 坑二:单例里塞了可变的全局状态
现象:系统出现诡异的偶发 bug,A 模块设置了配置项,B 模块读到了修改后的值,导致行为不一致;测试时单例状态在用例间互相污染。
原因:把单例当成了全局变量容器,任何地方都能往里塞值,破坏了封装,也引入了隐式耦合。
解决:单例只负责“创建和维护工作所需的不变配置”,比如连接池参数、环境名。所有可变状态应通过方法参数显式传递,不要作为单例的属性暴露。如果确实需要一个全局状态存储,那应该用事件机制或状态管理库,而不是裸单例。
5.3 坑三:工厂套工厂,调用链路黑匣子化
现象:一个请求从 Controller 进入,经过 Service、Factory、Builder、Provider,层层调用工厂,日志里只能看到一个接口名,根本不知道实际实例是什么,排查问题要打断点逐层看。
原因:滥用工厂,把每一个创建动作都套上抽象,导致调用链路过深。
解决:给工厂类规定一个边界:只有“外部输入的参数变化可能导致实现选择”时,才需要工厂。同一模块里,工厂层最多出现一层。排查时在工厂方法里通过日志打印实际创建的类名,比如log.info("factory created: {}", newThing.getClass().getSimpleName()),这样能快速定位链路。
5.4 坑四:策略模式接口设计过粗,子类被迫实现一堆用不到的方法
现象:一个策略接口里有 5 个方法,但某个新策略只需要用到其中 2 个,另外 3 个只能空实现或抛异常,代码里充满“不支持”的味道。
原因:接口抽取时没有按照“客户端需要的最小集合”来设计,而是按“所有策略可能的行为”来设计,导致接口过粗。
解决:先把策略接口拆成多个细粒度接口,或者用默认方法给出空实现,让子类只覆盖需要的方法。更稳妥的做法是,在选定策略前先画出“调用方会调用哪些方法”,让接口匹配调用方,而不是匹配实现方。必要时使用适配器模式,把不相关的策略包装成所需的接口。
5.5 坑五:观察者回调顺序导致状态不一致
现象:订单支付后,库存观察者先执行扣减,积分观察者后执行加分;某个时刻积分系统还没执行,前端已经查询到订单支付成功但积分没到账,用户反复刷新抱怨数据不对。
原因:观察者模式的默认通知顺序依赖注册顺序,而业务上存在先后依赖,这超出了观察者模式的适用边界。
解决:改换思路,不要在观察者里处理强一致的业务。把“必须保证顺序且同事务”的逻辑放到主题方法里串行执行,或者使用事件总线,给事件加上显式优先级。如果不想引入复杂框架,最简单的办法是定义OrderEvent包含一个eventType,订阅者自己判断是否需要处理,但这就变成了自己维护顺序,更推荐用组合模式管理一组有序订阅者。
6. 进阶:用模式评审代码,一张检查表帮你快速验证
当你已经知道模式该怎么用、容易踩哪些坑之后,最值钱的能力就变成了“评审别人的代码时快速判断模式是否用对”。我给自己准备了一张四问检查表,每次 review 设计模式相关代码时都会过一遍。
第一问:这个模式有没有对应一个明确的变化点?如果没有,它就应该被删掉。第二问:抽象接口的每个方法都有至少两个真实实现吗?如果没有,接口可能是在预支复杂度。第三问:调用方依赖的是接口还是具体类?如果调用方还在 new 具体类,那抽象可能只是摆设。第四问:新增一个功能,需要改动旧文件多少处?如果还是改很多处,模式没有起到该有的效果。
下面是用这张表评审一段代码的示例,你可以照着练习:
| 检查项 | 代码表现 | 结论 |
|---|---|---|
| 变化点 | 订单类型分支不断新增 | 策略模式合理 |
| 接口实现数量 | 策略接口有3个实现 | 暂不臃肿 |
| 调用方依赖 | 通过工厂拿策略 | 依赖抽象 |
| 新功能改动处 | 新增一个策略类,不改旧类 | 符合预期 |
除了检查表,我还有一个快速验证习惯:故意让某个实现抛异常,然后看运行时会报出哪个类、调用栈经过哪些抽象层。如果抛出的异常栈里有超过三层不想关的间接跳转,而且你需要花很久才能定位到具体实现,说明抽象层次放多了。此时你需要考虑把某层抽象拆掉,或者至少增加运行时日志。这个技巧很土,但比任何静态分析工具都直观。
最后说一个我自己的教训:刚接触设计模式时,我给一个支付模块加了工厂、策略、模板方法三层抽象,结果一次线上问题需要追支付渠道接入的配置来源,我顺着工厂找到实现,又从实现一路点开父类、接口,来回折腾了一个小时。后来我给自己定了个规矩——每个模式引入时,必须一行注释写清楚“它保护的变化方向是什么”。这个习惯救了很多人。从那以后,代码里的抽象少了,但每个抽象都站得住脚,同事找我 review 的次数也多了。希望帮到你。
本文还有配套的精品资源,点击获取