资讯动态

设计模式PDF精读指南:从原理到实战避坑,掌握单例策略工厂观察者

发布时间:2026/10/15 8:06:47 来源:尧图企业网站定制
简介王桂林《设计模式》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 ListOrderObserver 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; } }然后在订单服务里维护一个MapString, 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 坑二单例里塞了可变的全局状态现象系统出现诡异的偶发 bugA 模块设置了配置项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 的次数也多了。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑