资讯动态

创建型模式实战解析:从选型到避坑的完整指南

发布时间:2026/10/1 12:54:56 来源:尧图企业网站定制
1. 创建型模式到底在解决什么问题写业务代码写到一定阶段你一定会有这种感觉同一个对象在十几个地方被 new 出来参数稍微一变改得满屏飘红。说到根子上这是创建型模式缺位造成的。我当年接手过一个老项目光是一个订单状态的构造逻辑就散落在六个服务里每次加字段都要在群里吼一圈。后来把创建逻辑全部收敛到专门的工厂和构建器里世界才安静下来。这些工厂和构建器就是创建型模式的落地形态。创建型模式是设计模式中最基础的一大类负责解决三件事对象由谁来创建、对象怎么创建、什么时候创建。经典成员有五个单例、工厂方法、抽象工厂、建造者、原型简单工厂严格说不算 GoF 官方模式但使用频率最高我也放在文档里一起讲。创建型模式能帮你把调用方和具体类彻底解耦、控制实例数量、把复杂对象的组装过程收拢到一处。它适合正在写业务代码、被各种 new 整得焦头烂额的工程师也适合准备设计模式面试、想重构旧系统的开发者。1.1 代码越写越怕 new 的真实原因先从最原始的 new 说起。new OrderService()这种写法看似简单实际上在代码里埋了三个雷。第一是硬编码依赖调用方直接绑定了具体类想替换实现必须改所有调用点第二是构造参数重复一个对象要凑齐 10 个参数系统里每个调用方都要知道这 10 个参数从哪来第三是变化漂移对象创建逻辑一变所有调用点都得跟着动。用生活类比来说每次要杯咖啡都自己种咖啡豆、磨豆子、烧水那厨房迟早变灾难现场。创建型模式就是那台咖啡机原料进去成品出来你不关心内部是第几层研磨还是几度水温。它的本质是封装变化把对象创建这个最容易被修改的环节从业务代码里隔离出来。这样调用方只需要依赖稳定的抽象接口底下的具体类怎么换、怎么装配、有没有缓存全是创建者的事。一句话总结创建型模式的价值不在于消灭 new而在于让 new 只出现在它该出现的地方。1.2 六个模式怎么选先看这张选型地图很多新手学创建型模式第一个问题都是这么多模式到底什么时候用哪个我的判断方法很简单先问三个问题。第一这个对象在系统里允不允许出现多个不允许就单例。第二创建过程是一步到位还是分步组装分步组装用建造者。第三这个对象是一类产品还是一族产品一类产品用工厂方法配套产品用抽象工厂。创建成本特别高、想拿现成的改改就用原型。下面这张选型对照表我做技术方案时经常照着判断模式一句话选型标准典型应用场景单例全局唯一多实例会出问题配置中心、连接池、线程池、日志器工厂方法一个产品等级结构创建逻辑想下放给子类解析器工厂、Logger 工厂、驱动加载抽象工厂需要成套生产的兼容产品族跨平台 UI 组件、数据库访问族、皮肤主题建造者对象参数多、构建分步骤、需要校验配置对象、HTTP 请求构建器、SQL 构建器原型创建成本高复制比新建更划算对象拷贝、模板复制、撤销快照简单工厂产品类型少且稳定图省事小型工具库、常量映射、轻量对象创建这张表只是初筛具体还要结合代码组织方式来定。比如“产品类型少且稳定”这个条件一旦变成“产品类型经常新增”简单工厂马上就得升级成工厂方法或抽象工厂否则每次加产品都要改同一个类改到后面出现几十行 if-else连测试都不想给它写。2. 逐个拆解五个经典模式加一个高频变体这一章把五个经典创建型模式加上简单工厂挨个过一遍。我不打算像教科书那样背定义而是直接说清楚每个模式解决什么问题、代码长什么样、坑在哪里。学完这一章你至少能看懂项目里为什么会出现这些类也知道自己写的时候该叫什么名字。2.1 单例模式全局唯一但别把全局变量当宝贝单例的意图一句话保证一个类全局只有一个实例并提供一个统一访问点。标准结构是私有构造方法加静态实例加静态获取方法。私有构造是硬约束外部不能 new静态实例是缓存获取方法是唯一的出入口。实现方式里饿汉式最简单类加载时就创建实例天然线程安全缺点是类一旦加载就占用资源不管用不用都先建一个。懒汉式正好相反第一次调用才创建但多线程环境必须加锁。我推荐两种写法双重检查锁定DCL和静态内部类。前者追求极致性能后者最省心也能实现延迟加载。// 双重检查锁定DCL public class ConfigManager { private static volatile ConfigManager instance; private ConfigManager() { // 初始化配置 } public static ConfigManager getInstance() { if (instance null) { synchronized (ConfigManager.class) { if (instance null) { instance new ConfigManager(); } } } return instance; } }volatile 关键字是必须的不是可有可无。因为instance new ConfigManager()不是原子操作它分为分配内存、初始化对象、赋值三个步骤指令重排后可能先赋值后初始化另一个线程拿到半成品对象。加了 volatile 就禁止重排序保证语义正确。这是 DCL 容易忽略但非常关键的点。单例最容易被滥用的地方是把它当全局变量A 类里定义单例B 类直接通过 getInstance 存取一堆数据时间一长就变成隐式耦合。我的原则是单例只用来管理无状态或全局唯一资源的对象比如配置中心、连接池、线程池千万别拿单例做数据中转站不然改数据时连调用关系都理不清。2.2 工厂方法模式把 new 的决策权交给子类工厂方法解决的是“该创建哪个具体类”的问题。它的经典定义是定义一个创建对象的接口让子类决定实例化哪一个类。核心变化点在于工厂方法把“选哪个类”这个决策从调用方挪到了子类。结构上分四部分抽象产品、具体产品、抽象创建者、具体创建者。抽象创建者里声明工厂方法具体创建者重写这个方法返回对应的具体产品。// 产品接口 public interface Parser { Document parse(String content); } // 具体产品 public class JsonParser implements Parser { public Document parse(String content) { /* JSON 解析 */ } } public class XmlParser implements Parser { public Document parse(String content) { /* XML 解析 */ } } // 抽象创建者 public abstract class DocumentLoader { // 工厂方法由子类决定返回哪个解析器 protected abstract Parser createParser(); public Document load(String content) { Parser parser createParser(); return parser.parse(content); } } // 具体创建者 public class JsonDocumentLoader extends DocumentLoader { protected Parser createParser() { return new JsonParser(); } }注意这里有个容易混淆的点每次新增一种解析器都要新增一个具体创建者类。有人觉得麻烦但这恰恰是工厂方法的价值——新增类型时不需要改动已有代码符合开闭原则。代价是类数量会变多所以它只适合创建逻辑确实有分支、且分支可能继续扩展的场景。实战里我一般把工厂方法跟配置驱动结合。例如根据配置里的格式类型注册对应的 Loader再通过工厂方法拿到具体解析器。这样新增格式时只需要加一个产品类和一个创建者类再注册一下老代码一行都不用碰。如果分支永远不扩展那直接用简单工厂更省事别硬上工厂方法。2.3 抽象工厂模式成套生产别拆散一家人抽象工厂比工厂方法高一层它管的不再是单个产品而是一族相互关联的产品。最经典的场景是跨平台 UI 组件Windows 风格下有 WindowsButton、WindowsDialogMac 风格下有 MacButton、MacDialog。你不能让用户看到 Windows 风格的按钮配 Mac 风格的对话框这个组合约束就是抽象工厂存在的理由。// 抽象产品 public interface Button { void render(); } public interface Dialog { void show(); } // 具体产品Windows 风格 public class WindowsButton implements Button { public void render() { /* 绘制 Windows 按钮 */ } } public class WindowsDialog implements Dialog { public void show() { /* 显示 Windows 对话框 */ } } // 具体产品Mac 风格 public class MacButton implements Button { public void render() { /* 绘制 Mac 按钮 */ } } public class MacDialog implements Dialog { public void show() { /* 显示 Mac 对话框 */ } } // 抽象工厂每个工厂都能产出成套产品 public interface UIFactory { Button createButton(); Dialog createDialog(); } public class WindowsUIFactory implements UIFactory { public Button createButton() { return new WindowsButton(); } public Dialog createDialog() { return new WindowsDialog(); } } public class MacUIFactory implements UIFactory { public Button createButton() { return new MacButton(); } public Dialog createDialog() { return new MacDialog(); } }使用方拿到一个工厂比如 WindowsUIFactory调 createButton 和 createDialog 得到的必然是配套产品不用自己组合也不会出现风格错乱。这就是抽象工厂的核心收益把产品族的一致性约束收拢到工厂里而不是依赖调用方自觉。换句话说只要换一个具体工厂整个界面风格就全部切换调用方代码几乎不需要动。抽象工厂有一个著名的“扩展倾斜”问题增加一整套新产品族很容易比如再做一个 LinuxUIFactory只需实现一个类但要往产品族里加一个新成员比如要加 Card 组件就必须改抽象工厂接口所有具体工厂全部跟着改改动面非常大。所以抽象工厂适合产品族结构稳定的领域。如果产品成员经常增删就换个思路用注册表加工厂方法组合替代别在这个模式上死磕。2.4 建造者模式复杂对象的组装流水线建造者模式解决的痛点是对象参数太多、构建步骤有先后、还要做约束校验。这种对象用构造方法硬塞参数写出来就是一堆代码读起来更是灾难。它的核心思想是把“构建过程”和“最终表示”分离用同样的构建步骤产出不同的对象形态。最典型的例子是 Java 的 StringBuilder它是建造者模式在语言层面的设计还有 OkHttp 的 Request.Builder、Lombok 的 Builder 注解。我习惯用建造者来构建那些“必填项 若干选填项”的配置对象比如下面这段代码public class ReportConfig { private final String title; // 必填 private final int pageSize; // 选填默认 20 private final boolean showHeader; // 选填默认 true private ReportConfig(Builder builder) { this.title builder.title; this.pageSize builder.pageSize; this.showHeader builder.showHeader; } public static class Builder { private String title; private int pageSize 20; private boolean showHeader true; public Builder title(String title) { this.title title; return this; } public Builder pageSize(int pageSize) { this.pageSize pageSize; return this; } public Builder showHeader(boolean showHeader) { this.showHeader showHeader; return this; } public ReportConfig build() { if (title null || title.isEmpty()) { throw new IllegalArgumentException(标题不能为空); } if (pageSize 0) { throw new IllegalArgumentException(页码必须大于 0); } return new ReportConfig(this); } } }建造者模式有两个重点。第一流式 API 让调用方按语义一步步设置参数读代码的人看得懂第二build 方法适合做统一校验和默认值兜底把“缺参数才报错”的时机收拢到构建完成那一步而不是等运行到半截再爆异常。校验逻辑集中在一处测试也好写。建造者模式和工厂模式的区别我总结成一句话工厂关心的是造出哪一类产品一步到位建造者关心的是怎么一步一步把产品拼出来过程可控。如果一个对象构造起来就是三五个参数别用建造者直接构造方法或者手写一个参数对象更清爽。2.5 原型模式克隆一份比自己从零造划算原型模式的思路很直观拿现成的实例当模板通过拷贝来创建新对象。它的价值体现在两种场景第一种对象的创建过程成本很高比如需要查询数据库、加载文件、做复杂计算复制一份比从头再来便宜得多第二种运行时需要产生同一对象的多个状态快照比如撤销功能里的历史状态。Java 里实现原型需要实现 Cloneable 接口并重写 clone()但这里有个大坑clone() 默认是浅拷贝对象里的引用类型字段复制的是引用不是内容。改动克隆出来的对象可能连原对象一起改掉。public class TemplateConfig implements Cloneable { private String name; private MapString, String properties new HashMap(); Override public TemplateConfig clone() { TemplateConfig copy (TemplateConfig) super.clone(); copy.properties new HashMap(this.properties); // 手动深拷贝 return copy; } }需要深拷贝时可选的方案有三种手动逐字段拷贝、用 JSON 序列化后反序列化、用 Java 对象流序列化。手动拷贝性能最好但要维护每个字段JSON 序列化最简单但容易把 transient 字段和泛型类型信息搞丢对象流对嵌套对象友好但性能一般而且所有字段都得可序列化。我的建议是明确知道对象结构且性能敏感的用第一种其他场景用 JSON 序列化实现深拷贝代码量最少维护成本也最低。还有一个容易忽略的坑clone() 不调用构造函数所以依赖构造器做默认值初始化的类克隆出来的对象可能缺东西。我在账单系统里遇到过账单对象构造时会给 amount 初始化为 0克隆后直接从 null 开始报表一跑就炸。后面排查时才发现是克隆绕过了构造函数。2.6 简单工厂常被归入但值得单讲简单工厂不是 GoF 官方 23 种设计模式但实际项目里出镜率极高。它就是一个普通的工具类根据参数返回不同的产品实例。写法上比工厂方法直接得多public class ParserFactory { public static Parser create(String type) { if (json.equalsIgnoreCase(type)) { return new JsonParser(); } else if (xml.equalsIgnoreCase(type)) { return new XmlParser(); } throw new IllegalArgumentException(未知格式: type); } }简单工厂的优点是把产品创建集中到一处调用方不需要知道具体类缺点也一目了然每加一种产品就要改这个工具类不符合开闭原则。我的判断标准是产品类型少、变化慢、团队迭代不频繁用简单工厂完全没问题一旦出现“上周加一种、下周又加一种”的迹象赶紧升级成工厂方法把变的部分隔离出去别等到 if-else 分支多到没法看再动手。3. 动手实操跨平台 UI 组件库场景串联多个模式看完了六个模式各自的讲解接下来把它放进一个完整场景里看看这些模式怎么配合。我选的是跨平台 UI 组件库场景因为它同时用到抽象工厂、工厂方法、建造者和原型能把创建型模式串成一个完整方案。3.1 场景设计让多个模式各管一段假设我们要做一个报表系统需要渲染一组页面组件在 Web 端和移动端呈现不同的视觉风格。需求点有三个。第一页面上的按钮、对话框必须成套出现不能混搭否则产品经理会崩溃第二不同报表页面的按钮和对话框外形不同但构造步骤相同第三默认按钮样式创建成本高希望复制模板快速生成不同的按钮变体。这个场景的匹配结果很清晰抽象工厂负责产出成套的 Button 和 Dialog工厂方法通过一个 Provider 根据平台返回对应的具体工厂建造者负责一步步设置对话框的标题、内容、按钮文案原型负责复制默认按钮模板再微调样式。整个链路像一条乐高流水线每个模式负责一个环节。3.2 关键代码实现从抽象工厂到客户端调用第一步定义抽象产品和抽象工厂。这里我把 Button 和 Dialog 都定义成接口所有平台的具体组件都实现它们。第二步实现 Web 和移动端两套具体工厂。第三步用一个 Provider 根据平台类型拿到具体工厂这个 Provider 本质上就是简单工厂或工厂方法的入口。下面是一个可运行的 Java 示例骨架重点看代码之间的协作方式// 1. 抽象产品 public interface Button { void render(); } public interface Dialog { void show(); } // 2. 抽象工厂 public interface UIFactory { Button createButton(); Dialog createDialog(); } // 3. Web 风格实现 public class WebButton implements Button { private String style; public WebButton(String style) { this.style style; } public void render() { System.out.println(渲染 Web 按钮样式 style); } } public class WebDialog implements Dialog { private String title; private String content; public WebDialog(String title, String content) { this.title title; this.content content; } public void show() { System.out.println(Web 对话框 title - content); } } public class WebUIFactory implements UIFactory { public Button createButton() { return new WebButton(默认); } public Dialog createDialog() { return new WebDialog(提示, 内容); } } // 4. 移动端风格实现逻辑与 Web 相同只是具体组件类不同 public class MobileUIFactory implements UIFactory { public Button createButton() { return new MobileButton(默认); } public Dialog createDialog() { return new MobileDialog(提示, 内容); } }接着是工厂方法入口也就是 Provider。它对外屏蔽平台细节客户端拿到的永远只是一个 UIFactory具体是 Web 还是移动端调用方完全不用关心。这个 Provider 其实就是简单工厂的变体只不过它创建的不是最终产品而是工厂对象。好处是整个平台选择被收敛到一个点以后想加一种新的 UI 风格只需要在 Provider 里多注册一个分支调用链完全不动。public class UIFactoryProvider { public static UIFactory getFactory(String platform) { if (web.equalsIgnoreCase(platform)) { return new WebUIFactory(); } else if (mobile.equalsIgnoreCase(platform)) { return new MobileUIFactory(); } throw new IllegalArgumentException(不支持的平台: platform); } }到这一步客户端只用记住一件事想创建组件找 UIFactoryProvider 要一个 UIFactory然后调 createButton、createDialog。然后是建造者模式的落地。报表里的对话框参数不少直接用构造方法传参太过笨重所以用 DialogBuilder 一步步堆参数。这里有个细节要说清楚实际生产里DialogBuilder 会把攒好的 title、content 等参数传给具体工厂去构造完整 Dialog我这里为了保持示例简短直接用工厂生成的默认对话框重点展示构建过程的骨架public class DialogBuilder { private String title; private String content; private String confirmText; private String cancelText; public DialogBuilder title(String title) { this.title title; return this; } public DialogBuilder content(String content) { this.content content; return this; } public DialogBuilder confirmText(String text) { this.confirmText text; return this; } public DialogBuilder cancelText(String text) { this.cancelText text; return this; } public Dialog build(UIFactory factory) { // 实际生产代码里这里会用 title、content、confirmText、 // cancelText 等参数构造具体的 Dialog示例中省略参数赋值 return factory.createDialog(); } }原型模式在这个场景里的用法是把默认按钮保存为一个模板对象需要新按钮时克隆模板再修改个别样式字段。这样省掉了重新查配置、重新初始化样式的开销。示例里可以直接在具体按钮类上实现 Cloneable代码很简单就不再重复贴完整类了。核心就是老模板不动克隆出来的新对象随便改互不影响。3.3 参数选择与扩展方向什么时候该停下来这套结构看起来漂亮但不是没有代价。抽象工厂的产品族扩展很麻烦如果报表系统要新增 Card 组件抽象工厂接口要加 createCard()WebUIFactory 和 MobileUIFactory 都得同步实现改动面不小。这是抽象工厂固有的“扩展倾斜”问题设计阶段就要想清楚产品族会不会频繁加新成员如果会可以考虑用注册表加工厂方法替代抽象工厂把“组装成套产品”的逻辑交给一个注册中心按需返回。另一个常见调整是把工厂方法用配置驱动平台类型写在配置文件里通过注册表或者反射创建具体工厂。这样切换平台时不需要改代码只改配置发布系统里就剩一个 UIFactoryProvider 的入口。对于中小型项目这已经足够灵活。使用这套方案时还要守住一个原则不要为了“多模式”而堆模式。比如按钮创建逻辑在项目里很稳定就不需要原型对话框参数很少就不需要建造者。这套设计能成立是因为需求本身有配套产品、有复杂构建、有高成本实例这些特征。模式是跟着需求走的不是反过来。4. 血泪经验创建型模式的高频坑与排查技巧多写了几年代码就会发现理论是一回事实战是另一回事。这一章分享我在实际项目里踩过的坑和排查思路很多都是文档里不会写的东西但关键时刻比概念更有用。4.1 单例的线程安全与序列化陷阱单例最常翻车的地方是多线程下的懒加载实现。很多人把 synchronized 直接加在 getInstance 方法上性能瓶颈明显也有人在 DCL 里忘了 volatile结果高并发下时不时拿到半初始化对象这种 bug 非常难查。我排查时第一步不是看代码而是先在压测环境压一轮如果出现“偶发空指针”九成是单例初始化问题。接着再看实例字段有没有被部分赋值定位到具体是哪一步没执行完。另一个连环坑是序列化。单例类实现 Serializable 之后反序列化会绕过私有构造直接生成新实例破坏单例。解决办法是在类里加 readResolve() 方法返回现有实例protected Object readResolve() { return getInstance(); }还有更高阶的坑多个类加载器。在 Web 容器里如果同一个类被多个类加载器加载单例会变成多个。排查方式是在 getInstance 里临时打日志输出 ClassLoader或者直接看堆转储里对象数量数一数就知道是不是被加载了多份。这种现象在 OSGi 和复杂容器环境尤其常见平时开发跑不起来根本遇不到。4.2 工厂模式被滥用的信号工厂模式被滥用的情况比不用更常见。我见过一个项目一个产品只有一个实现也硬套了工厂结果调用链从 Controller 到 Service 到 DAO 全是 factory.create代码评审时根本无从下手。记住工厂存在的意义是有“变化点”没有变化就没有工厂。如果一个类在可见的未来只有一种实现直接用构造方法等出现第二种实现再考虑工厂完全来得及。第二个被滥用的信号是简单工厂里写了几十行 if-else还不停往里面塞新产品。出现这种情况要么升级工厂方法把分支下放给子类要么用依赖注入容器管理对象创建。还有一个信号是工厂方法嵌套工厂方法链路过长调试时根本分不清是哪个工厂创建的。我给自己定的规矩是工厂方法最多嵌套两层超过两层就考虑抽一个独立的 FactoryRegistry 来管理让调用关系一目了然。4.3 原型模式的深拷贝问题原型模式最容易踩的坑就是浅拷贝。有一个特别直接的验证方法克隆一个对象改它的 List 字段如果原对象也跟着变了说明浅拷贝了。这个坑在单元测试里就能轻松发现所以写完克隆逻辑一定要补一个针对性测试别等到线上用户操作出问题再回头查。解决方案不要一上来就引一堆深拷贝库先看对象结构。如果是简单的 DTO手动建个新对象复制字段最靠谱如果对象嵌套很深用 JSON 序列化反序列化几百行代码变两行。但要注意 JSON 序列化无法处理循环引用对象图里有环时必须兜底保护否则直接栈溢出。我见过一个配置中心项目就是图省事用 JSON 深拷贝结果配置文件里一个自引用字段把整个接口打挂后来被迫手写了深拷贝方法。另外我前面提到过clone() 方法不调用构造函数依赖构造器做默认值初始化的类克隆出来的对象可能缺东西。排查方式是用反射对比构造器和 clone 后的字段差异或者更简单在测试里把两种方式生成的对象逐个字段断言一遍。这种问题很难靠肉眼发现跑一遍自动对比最踏实。4.4 评审与面试中的高频追问创建型模式这块评审和面试基本绕不开几个问题。第一个单例怎么保证线程安全、怎么防反射反射破坏的解法是私有构造里判断 instance 不为空就抛异常枚举单例天然免疫。第二个工厂方法和抽象工厂有什么区别一句话工厂方法生产单一产品一个工厂对应一个产品等级结构抽象工厂生产产品族一个工厂对应一组关联产品。第三个建造者和工厂的区别是什么工厂一步到位建造者分步可控还负责校验。第四个原型模式什么时候用创建成本高、状态相似、需要快照。第五个简单工厂为什么不算是官方模式因为它没有提供抽象层职责集中违反开闭原则。第六个创建型模式中最容易被滥用的是哪个我的答案永远是单例和工厂前者容易退化成全局变量后者容易为了用而用。面试官通常还会追问一个开放题“你现在这个系统里如果产品族要新增一个成员影响面有多大”这个问题直接考察你是否理解抽象工厂的扩展倾斜。能答出“需要修改抽象工厂接口所有具体工厂都要同步实现”的候选人才算真正理解而不是背了四个角色名字就出来面试。我个人在实际项目里的体会是创建型模式从来不是越多越好。设计评审的时候我一般先问三个问题对象创建时依赖哪些外部条件这些条件未来会不会变创建过程需不需要调用方干预想清楚这三点选型基本不会跑偏。还有一个实用技巧别急着引入抽象工厂和建造者先用简单工厂把创建逻辑收拢等真的出现第二种产品族、出现参数足够复杂的对象再升级成对应的模式。重构成本远比一开始就猜测需求、堆满设计模式来得低。

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

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

免费获取报价 →
↑