
1. 23种设计模式到底该怎么啃先搭骨架再填肉软考软件设计师的教材翻到第七章23种设计模式黑压压排在那里第一次看的人十有八九会卡住名字都认识合上书一个都说不出来。我当年备考软件设计师中级也是这么过来的第一遍硬背第二天全忘第二遍换了方式先把分类骨架画出来再往格子里填模式才算真正立住。这一章在软考软件设计师里属于必考的硬骨头上午题稳定出选择题软件设计师下午题往往把设计模式藏在类图、用例图、状态图的分析里甚至直接问你此处采用哪种设计模式最合适。所以它真不是背概念就能过的知识点而是要能讲故事、能落代码、能判断场景的东西。先把结论摆出来23种设计模式不是23个孤立的名词它是23种变化点的封装箱。你只要抓住每一类模式想解决的问题类型回忆时就有了钩子答题时就有了判断依据。下面我按创建型—结构型—行为型这条主线把每个模式拆到能写代码、能画类图、能回答下午题的程度再把踩过的坑和易混点一起摆出来。1.1 软考软件设计师为什么盯着设计模式不放设计模式本质上是前人踩坑后的可复用方案。它考查的不是你背了多少名字而是你有没有面向对象的设计意识面对一个需求变更你是改一堆 if-else还是用策略模式把变化隔离出去面对对象创建的复杂流程你是 new 到底还是交给工厂统一收口。这些能力在上午题里以概念辨析的形式出现在下午题里以补全类图关系选择合适模式的形式出现甚至真题里出现过让你根据描述判断使用了哪种设计模式的题目。从命题规律看上午题对设计模式的考查有两个偏好一是考意图也就是这个模式用来干什么二是考结构比如类与类之间是继承还是聚合是组合还是依赖。下午题则更看重应用常出现在系统分析与设计的第一题里给你一段业务场景描述让你识别出哪些地方适合抽象、哪些关系应该画成聚合。这两类考法对应的是两种完全不同的复习方式概念要背结构要画场景要练。另外还有个现实因素中级软件设计师的知识点总结里设计模式是和 UML 图、面向对象基础强绑定的模块。UML 类图里的泛化、实现、聚合、组合、依赖、关联这六种关系几乎就是设计模式的结构语言。你如果连聚合是空心菱形、组合是实心菱形都分不清那看到组合模式、桥接模式的类图一定懵。所以我的建议是复习设计模式前先花半天把 UML 类图六种关系过一遍这一步的投入产出比高得离谱。1.2 按创建—结构—行为三分法建立记忆坐标GoF 那本经典著作把 23 种模式分成三组这个分法不是为了排版好看而是为了回答三个不同的问题创建型5种对象怎么造出来。关注的是用谁、怎么用、什么时候造包括单例、工厂方法、抽象工厂、建造者、原型。补充一句简单工厂模式虽然没进 GoF 23 种但在软考和设计模式期末考里出现频率极高必须一起掌握。结构型7种类与对象怎么拼起来。关注的是接口不兼容怎么办、想加功能又不想改原类怎么办包括适配器、桥接、组合、装饰、外观、享元、代理。行为型11种职责怎么分配、算法怎么切换、对象之间怎么通信。包括责任链、命令、解释器、迭代器、中介者、备忘录、观察者、状态、策略、模板方法、访问者。我把这个坐标记成一句话造、拼、管。造对象看创建型拼结构看结构型管职责看行为型。考试时拿到一个场景先问自己一句它纠结的是造、是拼、还是管答案就筛掉一大半。1.3 我在复习中用过的三轮滚动法纯背 23 个名字的遗忘曲线非常陡我实测过不复习的情况下第三天只剩不到三成。后来改成三轮滚动效率高很多具体做法如下轮次目标具体动作单轮耗时第一轮记住分类和意图只看每类的定义和一句话意图画一张分类树2 小时第二轮记住结构每个模式手画一遍类图标出继承/聚合关系每天 40 分钟持续一周第三轮能做场景判断拿真题里的描述题练先判断再对答案每天 20 道主观判断第一轮只求知道有这个模式、它是干哪一类事千万别一上来就抠代码实现。第二轮才是关键因为软考上午题很多选项就是拿两个相似模式的类图来对比你画过就不会被绕。第三轮练的是下午题的实战感很多时候题目不会明说请使用某某模式而是描述一堆业务规则让你自己判断该抽象什么。这里有个小技巧画类图时用不同颜色区分抽象层和实现层。设计模式的共同套路基本都是面向抽象编程把变化隔离在实现层你把这个意识画进肌肉记忆里比背文字管用得多。注意三轮不是三次就完事第二轮和第三轮要交叉滚动。每周周末抽半小时把上一周画过的类图默写一遍忘得最快的往往不是定义而是类与类之间的那根线。2. 创建型模式5个模式落地时最容易翻车的细节创建型模式的核心就一句话把new这个动作从业务代码里赶出去。为什么要把 new 赶出去因为对象一旦在业务里裸 new你就把具体实现焊死在逻辑里了将来换个实现、加个缓存、加个日志就得满地找 new 去改。工厂家族解决的正是这个问题。下面把 5 个创建型模式逐个拆开重点讲实际写代码时容易错的地方。2.1 单例模式五种写法和线程安全那点事单例模式保证一个类只有一个实例并提供全局访问点。听着简单写对不容易因为它踩的是并发和反射两个坑。先看最基础的饿汉式public class ConfigManager { private static final ConfigManager INSTANCE new ConfigManager(); private ConfigManager() {} public static ConfigManager getInstance() { return INSTANCE; } }饿汉式代码最干净类加载时就创建实例天生线程安全缺点是如果实例很重、启动时又不一定用得上就会浪费一次初始化。软考选择题里常考它的特点线程安全、加载即创建、可能造成资源浪费。懒汉式则是在第一次调用时才创建但朴素的写法在并发下会造出多个实例public class LazySingleton { private static LazySingleton instance; private LazySingleton() {} public static synchronized LazySingleton getInstance() { if (instance null) { instance new LazySingleton(); } return instance; } }直接给方法加 synchronized 虽然安全但每次调用都要抢锁性能差。改进版是双重检查加 volatilepublic class DclSingleton { private static volatile DclSingleton instance; private DclSingleton() {} public static DclSingleton getInstance() { if (instance null) { synchronized (DclSingleton.class) { if (instance null) { instance new DclSingleton(); } } } return instance; } }这里的 volatile 不是可选项。对象创建分三步分配内存、初始化、赋引用JVM 可能把第二步和第三步重排序导致另一个线程拿到一个地址有值但对象还没初始化完的半成品。不加 volatile双重检查在极端并发下就是错的。这个点在面试和下午题里都是经典考点。还有两种更省心的写法。静态内部类利用类加载机制保证线程安全又实现了懒加载public class HolderSingleton { private HolderSingleton() {} private static class Holder { private static final HolderSingleton INSTANCE new HolderSingleton(); } public static HolderSingleton getInstance() { return Holder.INSTANCE; } }枚举写法则能天然防住反射和反序列化破坏单例这是《Effective Java》里推荐的方式代码最短public enum EnumSingleton { INSTANCE; public void doSomething() { } }单例的使用场景很好记配置管理、连接池、日志对象、缓存、线程池凡是全局只需要一份、创建成本高的都算。但单例也有副作用它本质上是全局状态会让单元测试变难依赖关系被隐藏起来。所以别把它当成万能工具能用依赖注入解决的就别硬上单例。注意单例的构造函数一定要私有化否则外部照样能 new。另外序列化和反射是可以绕过私有构造的考试里问到单例会被什么破坏答案就是反射、序列化、克隆这三条路。2.2 简单工厂、工厂方法、抽象工厂的层层递进这三个是软考最容易出对比题的地方因为它们长得像但适用场景完全不同。简单工厂一个工厂类负责根据参数创建不同产品。public class ShapeFactory { public static Shape create(String type) { if (circle.equals(type)) return new Circle(); if (rect.equals(type)) return new Rectangle(); throw new IllegalArgumentException(unknown type); } }它的好处是客户端不用管具体类坏处是新增产品必须改工厂里的 if-else违反开闭原则。简单工厂不算 GoF 23 种之一但工程里用得特别多尤其是配合配置文件做反射创建对象时。工厂方法把创建动作延迟到子类。工厂接口只定义造什么的契约具体由每个子工厂决定。interface ShapeFactory { Shape create(); } class CircleFactory implements ShapeFactory { public Shape create() { return new Circle(); } } class RectFactory implements ShapeFactory { public Shape create() { return new Rectangle(); } }新增产品时只需要新增一个工厂类不动老代码开闭原则满足了。代价是类的数量翻倍产品多的时候类会爆炸。抽象工厂面向产品族。当系统需要创建一整套相互关联的产品时用它比如同一套 UI 皮肤里的按钮、文本框、滚动条必须风格统一那就用一个工厂接口一次性把这些产品都造出来。interface SkinFactory { Button createButton(); TextBox createTextBox(); } class DarkSkinFactory implements SkinFactory { public Button createButton() { return new DarkButton(); } public TextBox createTextBox() { return new DarkTextBox(); } }三者的核心差异我用一张表锁死模式解决什么问题扩展产品扩展产品族复杂度简单工厂一个工厂造多种产品要改工厂代码不涉及低工厂方法一种产品多种实现加工厂类即可不涉及中抽象工厂一整套产品族统一要改工厂接口加产品族即可高记抽象工厂有个窍门工厂方法是一维的抽象工厂是二维的。前者只在一个产品线上做文章后者是产品线按钮/文本框乘以风格深色/浅色的二维矩阵。2.3 建造者与原型复杂对象与克隆建造者模式适合对象由很多零件拼出来、拼装顺序还有讲究的场景比如一份合同文档有页眉、正文、附件、签名区又比如组装一台电脑要选 CPU、内存、硬盘。它的写法是把构建过程拆成一步步的调用最后一次性产出对象public class Computer { private String cpu; private String ram; private String disk; public static class Builder { private Computer c new Computer(); public Builder cpu(String v) { c.cpu v; return this; } public Builder ram(String v) { c.ram v; return this; } public Builder disk(String v) { c.disk v; return this; } public Computer build() { return c; } } }链式调用读起来像句子这是它最大的优点。软考里建造者常和重叠构造器JavaBean 的 setter放在一起对比考点是它能保证对象在构建完成前不被使用也不会出现参数顺序传错的低级错误。原型模式的核心是克隆。当创建一个对象的成本远高于复制现有对象时就用原型。要注意浅克隆和深克隆的区别浅克隆只复制基本类型字段引用类型还是指向同一块内存改一个另一个也会跟着变深克隆要把引用对象也复制一份。Java 里实现 Cloneable 接口并重写 clone 就是浅克隆深克隆一般用序列化反序列化来做。public class Prototype implements Cloneable { private ListString tags new ArrayList(); Override public Prototype clone() throws CloneNotSupportedException { Prototype p (Prototype) super.clone(); p.tags new ArrayList(this.tags); // 手动深克隆引用字段 return p; } }创建型五个模式对比下来选择逻辑其实很清晰单一实例用单例按类型灵活创建用工厂成套创建用抽象工厂分步构建用建造者复制现有对象用原型。考试时看题干里出现唯一按参数一族分步复制这些关键词基本就能锁定答案。3. 结构型模式7个模式解决类与对象怎么拼结构型模式解决的是组合问题。业务一变类的结构就得调结构型模式的思路是与其改结构不如在中间加一层让变化被这层挡住。这七个模式里适配器、装饰器、代理三个长得特别像是选择题的重灾区我会重点拆。3.1 适配器、桥接、组合从接口不兼容到树形结构适配器模式解决的是现成的类接口不匹配的问题。典型场景是项目里已经有一套老的支付接口新业务要接一个新协议你不想改老代码就写个适配器把新接口翻译成老的调用形式。它有两种写法类适配器用继承对象适配器用组合。软考选择题偏爱对象适配器因为组合比继承更灵活。interface Target { void request(); } class Adaptee { public void specificRequest() { } } class Adapter implements Target { private Adaptee adaptee; public Adapter(Adaptee a) { this.adaptee a; } public void request() { adaptee.specificRequest(); } }桥接模式解决的是两个维度各自变化、组合起来会爆炸的问题。经典例子是图形与颜色图形有圆、方、三角颜色有红、蓝、绿如果用继承就得写九个类桥接把颜色抽成独立维度让图形持有颜色引用类数量降到三加三。interface Color { String apply(); } abstract class Shape { protected Color color; public Shape(Color c) { this.color c; } public abstract void draw(); }桥接的关键词是抽象与实现分离两个独立变化的维度题目里一旦出现两个方向都要扩展优先想它。组合模式用来表达整体—部分的树形结构让客户端对单个对象和组合对象的使用方式一致。文件系统、组织架构、菜单树都是典型场景。它有个要点叶子节点和容器节点实现同一个接口但叶子节点在 add/remove 这类方法上要么抛异常要么空实现这是透明组合和安全组合两种写法的区别。abstract class Node { public void add(Node n) { throw new UnsupportedOperationException(); } public abstract void print(int depth); } class FileNode extends Node { public void print(int depth) { System.out.println(file); } } class DirNode extends Node { private ListNode children new ArrayList(); public void add(Node n) { children.add(n); } public void print(int depth) { children.forEach(c - c.print(depth 1)); } }3.2 装饰器、外观、享元、代理增强、简化、复用与管控装饰器模式在不改原类的前提下动态给对象加功能。它的结构特征是装饰器实现和被装饰对象一样的接口同时持有一个被装饰对象的引用。IO 流是它的教科书案例BufferedReader 包住 FileReader 就是装饰。interface Coffee { double cost(); } class SimpleCoffee implements Coffee { public double cost() { return 10; } } class MilkDecorator implements Coffee { private Coffee inner; public MilkDecorator(Coffee c) { this.inner c; } public double cost() { return inner.cost() 3; } }装饰器最大的价值是避免了为了加功能而写一堆子类。想加奶加糖只需层层包装而不是去写加奶咖啡加糖咖啡加奶加糖咖啡这种组合爆炸的子类。外观模式是给复杂子系统提供一个统一入口。它的动机很朴素一个下单操作背后要调库存、支付、物流、通知四个子系统客户端不该知道这些细节于是包一个 Facade 类暴露一个 placeOrder() 就完事。外观不禁止客户端直接访问子系统它只是提供一条更省事的路。享元模式靠共享减少内存占用核心是把对象的内部状态可共享、不变的和外部状态随场景变的、要外部传进来的拆开。典型例子是文本编辑器里的字符对象字符本身可共享位置和字号作为外部状态传入。它的关键是享元工厂加一个缓存池。代理模式给对象提供一个代理来控制访问。按用途分有远程代理、虚拟代理、保护代理、智能引用代理。虚拟代理用来延迟加载大对象保护代理用来做权限校验智能引用代理可以在方法调用前后加日志和计数。interface Service { void work(); } class RealService implements Service { public void work() { System.out.println(real); } } class LogProxy implements Service { private Service target; public LogProxy(Service t) { this.target t; } public void work() { System.out.println(before); target.work(); System.out.println(after); } }装饰器、代理、适配器这三兄弟的区别我整理成下面这张表考试基本就靠它了模式目的接口是否改变典型场景适配器让接口兼容改变翻译老系统对接新协议装饰器动态加功能不变增强IO 流层层包装代理控制访问不变拦截权限校验、延迟加载一句话区分适配器是翻译装饰器是加料代理是看门。这句口诀我用了很多年考场上比翻书快。4. 行为型模式11个模式管的是职责怎么分行为型模式数量最多也最容易混。它们关注的不是对象怎么造、怎么拼而是对象之间怎么分工、怎么通信、算法怎么切换。软考上午题里策略、状态、模板方法、观察者、责任链这几个出现频率最高。4.1 责任链、命令、解释器、迭代器责任链模式把处理者串成一条链请求沿着链传下去直到有人处理。它的价值是解耦发送者和接收者让多个对象都有机会处理请求。审批流是最典型的场景主管批不了就往上交给经理经理批不了再交给总监。abstract class Handler { protected Handler next; public Handler setNext(Handler n) { this.next n; return n; } public abstract void handle(int amount); } class Manager extends Handler { public void handle(int amount) { if (amount 5000) System.out.println(manager ok); else if (next ! null) next.handle(amount); } }链的组装顺序很关键顺序错了业务规则就错了这是实际开发里最容易出错的地方。另外要注意防止链过长导致性能问题以及忘设终止条件造成空指针。命令模式把请求封装成对象从而支持撤销、重做、排队、日志。它的结构包含四部分命令接口、具体命令、接收者、调用者。遥控器是经典类比按下按钮这个动作被封装成命令对象命令对象再去调电视的开关方法。解释器模式给一门简单语言定义文法并解释执行比如表达式求值、规则引擎里的简单条件。它用得很少考试一般只考概念和适用场景是简单文法、效率不是首要考虑。迭代器模式把遍历逻辑从集合里抽出来让客户端用统一的 hasNext/next 访问不同集合。它的意义是让集合的内部结构对外不可见同时支持多种遍历方式。4.2 中介者、备忘录、观察者中介者模式用一个中介对象封装一组对象之间的交互把网状的多对多关系变成星形的一对多。聊天室是经典例子每个人不直接和别人通信而是都发给聊天室由聊天室转发。它的好处是降低了类之间的耦合坏处是中介者本身可能变得非常庞大成为新的上帝类。备忘录模式在不破坏封装的前提下保存对象内部状态以便将来恢复。它有三个角色发起人、备忘录、管理者。撤销功能、游戏存档、数据库事务回滚都是它的应用。这里有个取舍备忘录存得越多内存占用越大所以实际工程里往往配合增量保存或者只保存最近几次。观察者模式建立一对多的依赖关系被观察者状态变化时自动通知所有观察者。事件监听、消息订阅、MVC 里的模型通知视图都属于它。软考里它常和 MVC 设计模式一起考你要清楚 Model 变化后是通过观察者机制通知 View 的这样才不违反分层原则。interface Observer { void update(String msg); } class Subject { private ListObserver observers new ArrayList(); public void attach(Observer o) { observers.add(o); } public void notifyAll(String msg) { observers.forEach(o - o.update(msg)); } }观察者有个坑如果观察者持有被观察者的强引用列表又不清理就容易造成内存泄漏。实际项目里做事件总线时要记得在对象销毁时取消订阅。4.3 状态、策略、模板方法、访问者这四个是上午题的高频考点尤其是状态和策略的区分几乎每年都有人栽。策略模式把一组算法封装起来让它们可以互相替换。调用方持有一个策略接口运行时选择具体策略。它适用于多种算法选一种的场景比如不同的排序、不同的计费规则、不同的促销计算。状态模式让对象在内部状态改变时改变它的行为看起来像是换了一个类。它适用于对象的行为依赖状态且状态有明确转换关系的场景比如订单状态、连接状态、审批状态。两者结构几乎一样区别在意图策略模式的各个算法之间是平等的客户端主动选择状态模式的状态之间有转换关系状态自己会驱动转移。题目里如果出现状态切换自动流转那是状态模式如果出现根据条件选择不同算法那是策略模式。模板方法模式在父类里定义算法骨架把可变步骤延迟到子类实现。它是最简单的行为型模式也是工程里最常见的。做数据处理时读取、校验、转换、写出的流程固定只有转换规则因业务而异那就把转换抽成抽象方法。abstract class DataPipeline { public final void run() { read(); validate(); transform(); write(); } protected abstract void transform(); protected void read() { System.out.println(read); } protected void validate() { System.out.println(validate); } protected void write() { System.out.println(write); } }注意 run() 上加 final 是常见写法防止子类改掉算法骨架。软考里常考的点是模板方法使用继承策略模式使用组合这是两者最本质的差别。访问者模式把作用于对象结构中各元素的操作分离出来新增操作时不用改元素类。它的代价是新增元素类型很麻烦因为要改所有访问者。所以它适合元素结构稳定、操作经常增加的场景比如编译器对语法树做类型检查、代码生成等多种遍历。5. 常见问题与排查技巧实录前面把 23 个模式按类拆完了但真正上考场、真正写代码的时候问题往往出在分不清。这一节把我踩过的坑和常被问到的问题集中摆出来。5.1 最容易混淆的六组模式对照第一组工厂方法和策略模式。工厂方法关注造对象策略关注选算法虽然都面向接口但目的不同。第二组抽象工厂和建造者。抽象工厂一次造一整套产品建造者分步骤造一个复杂对象。看到分步骤、有顺序就想建造者。第三组状态和策略。上面说过看有没有状态自动流转。第四组装饰器和代理。装饰器是为了增强功能代理是为了控制访问。考试里如果描述是在不改变原类的情况下增加职责是装饰器如果是在访问前后做额外处理比如日志、权限、延迟加载是代理。第五组适配器和桥接。适配器是接口已经存在但不兼容事后补救桥接是设计之初就把两个维度分开事前规划。第六组中介者和外观。外观是单向的客户端调子系统中介者是双向的同事对象之间通过中介者互相通信。易混组合一句话区分工厂方法 vs 策略造对象 vs 选算法抽象工厂 vs 建造者造一套 vs 造一个状态 vs 策略自动流转 vs 主动选择装饰器 vs 代理加功能 vs 控访问适配器 vs 桥接事后补救 vs 事前分离中介者 vs 外观双向通信 vs 单向调用5.2 软件设计师下午题里设计模式怎么考下午题不会直接给你一个空让你填这里用某某模式它更常这样考给你一段业务描述让你画类图并说明类之间的关系。这时候判断依据是关系类型而不是模式名字。看到一种实现有多种算法就想泛化和接口看到由多个部件组成就想组合或聚合看到对外提供统一入口就想外观。另外下午题第一题经常考数据流图和用例图看上去跟设计模式无关但里面的分层思想是一致的。用例图里泛化关系表示参与者或用例的一般与特殊类图里泛化表示继承本质是同一套抽象思维。软件设计师中级真题里用例图相关的题目很多考生丢分不是不会画而是忘了连线要标箭头和关系类型。答题时我建议养成一个固定顺序先通读需求划出名词候选类再划出动词候选方法然后判断哪些类是抽象、哪些是具体最后才去挑模式。先想模式再看题特别容易被误导因为题目作者经常故意放一个看起来像却不对的场景。5.3 我踩过的坑与几条实操心得第一条别为了用模式而用模式。我刚开始写代码时一个小功能也硬套工厂加策略结果几百行代码里塞了十几个类维护成本比直接写 if-else 高得多。设计模式是解药也是毒药只有在变化点确实存在、且未来大概率会扩展时才值得引入。第二条模式不是越多越好能合并就合并。工厂方法在只有一种实现时可以退化成简单工厂策略在只有两种固定算法时也完全可以先用条件判断。过度设计在评审时经常被直接驳回。第三条写代码先写接口再写实现。设计模式共同的骨架就是面向抽象编程你养成先定接口的习惯很多模式会自然浮现而不是靠硬套。第四条复习时一定要手写类图。眼睛看一遍和手里画一遍是完全不同的记忆强度。我复习时把 23 个模式每个都画了一张类图贴在书桌前一周下来闭眼就能还原结构这对上午题的选择题帮助极大。第五条注重组合优于继承这条原则。结构型和行为型里大量模式都是靠组合实现灵活的考试里描述通过持有对象引用而不是继承来扩展功能基本就是组合相关的模式。第六条别忽略简单工厂和 MVC。简单工厂虽然不在 GoF 23 种里但在软件设计师考试里出现得比抽象工厂还勤。MVC 也不是 GoF 模式但它把观察者、策略、组合等模式揉在一起是理解模式协作的最佳案例。最后分享一个我自己做题的小习惯每道涉及设计模式的题我都在草稿纸边上写一个字造、拼、还是管然后用排除法。这个动作只要两秒但能显著降低被相似选项带偏的概率。23 种设计模式看着吓人拆成三组、抓住每组的核心意图、再配合真题练几轮其实没有想象中那么难啃。真正难的不是记住名字而是遇到一个新场景时能判断出变化点在哪、该用哪种方式把变化关起来。