
1. 为什么创建型设计模式值得认真对待1.1 对象创建为什么需要模式很多人一开始接触设计模式第一反应是“这不就是教我怎么 new 对象吗”。这句话没错但只说对了一半。创建型设计模式研究的是“如何更优雅地 new 对象”核心目的是把“创建对象”这件事从业务代码里抽离出来让调用方不用关心对象是怎么拼装、怎么初始化、怎么保证唯一性的。举一个我在实际项目中遇到的例子早期接手过一个订单系统里面到处是Order order new Order();后来订单模型加了几个必填字段比如优惠券信息、来源渠道、分销关系结果所有 new Order 的地方全部编译报错一行一行改改了整整一个下午。这就是典型的创建逻辑和业务逻辑耦合的代价。创建型模式要解决的就是这种“对象的组装过程变化时不要炸到一片调用方”的问题。如果你只是写几十行的工具脚本那确实不需要这些模式。但一旦项目规模上来对象关系变复杂或者对象的创建流程经常调整创建型模式的价值就非常明显。它能帮你把变化隔离在一个地方让新增逻辑时只改一个类而不是满项目找引用。1.2 创建型模式的分类和选型思路创建型模式一共有五种简单工厂也叫静态工厂、工厂方法、抽象工厂、单例、建造者、原型。严格来说简单工厂不是 GoF 23 种设计模式里的正式成员但因为太常用几乎所有资料都会把它放在创建型这一章讲。再加上原型模式实际上一共六种解决方案只是 GoF 把简单工厂归入了工厂方法的一种特例。选型的时候我习惯先问自己几个问题创建的对象是单个产品还是多个产品组成的“产品族”创建逻辑是会频繁变化还是基本稳定这个对象在系统里允许多个实例吗对象的构造过程有多复杂参数多不多是“慢创建”还是“快创建”克隆会不会比 new 更合适这几个问题的答案基本决定了你会用哪种模式。简单工厂适合产品类型少、变化不频繁的场景工厂方法适合把创建逻辑下沉到子类的场景抽象工厂适合横向扩展产品族单例适合全局唯一资源建造者适合字段多且必填项多的对象原型适合创建成本高但复制成本低的场景。下面逐个拆开讲。2. 工厂三兄弟简单工厂、工厂方法、抽象工厂2.1 简单工厂最常用但别滥用简单工厂的核心思想是在一个工厂类里用 if 或 switch 根据参数返回不同的产品对象。实际项目里最常见的写法是这样的public class PaymentFactory { public static Payment create(String channel) { if (alipay.equals(channel)) { return new Alipay(); } else if (wechat.equals(channel)) { return new WechatPay(); } else if (unionpay.equals(channel)) { return new UnionPay(); } throw new IllegalArgumentException(未知的支付渠道: channel); } }调用方只需要写Payment payment PaymentFactory.create(alipay);完全不用管 Alipay 内部怎么初始化。这样做的最大好处是所有支付渠道的创建逻辑都收拢到一个类里新增渠道时只需要改工厂的 if 分支调用方代码一行不用动。但这里有个陷阱简单工厂对“开闭原则”是不友好的。每新增一个产品你都要改工厂类的代码如果产品类型特别多这个工厂会膨胀成一个巨大的 if 分支集合又臭又难维护。我见过一个项目里的工厂 switch 分支超过 40 个 case每次改动都要小心翼翼生怕影响其他分支。所以我的建议是简单工厂只适合产品种类少、扩展频率低的场景比如枚举类驱动的策略对象。一旦发现工厂里分支超过十几个或者每周都要加新产品就该考虑用工厂方法重构了。另外工厂方法的返回值建议定义为接口或抽象类而不是具体类这样调用方依赖的是抽象而不是实现后面替换实现类的时候更灵活。2.2 工厂方法把创建逻辑交给子类工厂方法模式把“创建对象”这件事定义为抽象方法由子类决定具体实例化哪个类。它和简单工厂最大的区别是简单工厂把创建逻辑集中在单个类里工厂方法把创建逻辑分散到多个工厂子类里。看一个实际场景系统需要支持多种数据库连接Mysql、Postgres、Oracle。如果用简单工厂必然会有create(String type)加一堆 if。用工厂方法设计就变成public interface DatabaseFactory { Connection createConnection(); } public class MysqlFactory implements DatabaseFactory { Override public Connection createConnection() { return new MysqlConnection(); } } public class PostgresFactory implements DatabaseFactory { Override public Connection createConnection() { return new PostgresConnection(); } }调用方依赖DatabaseFactory接口具体使用哪种数据库在启动时配置好对应的工厂对象即可。新增数据库时只要写一个新的工厂类不需要改已有的任何代码完全符合开闭原则。当然工厂方法也有代价类的数量翻倍了。每个产品都要对应一个工厂类如果产品很多类会爆炸。所以工厂方法一般用在“产品创建逻辑本身有差异”的场景而不是所有产品都只是 new 一下的区别。如果所有产品的创建逻辑都一样又不需要扩展那简单工厂反而更简洁。设计模式不是越多越好而是恰好解决问题最好。2.3 抽象工厂解决产品族的创建抽象工厂是我认为创建型模式里最容易理解错的一个。它不是为了创建单个对象而是为了创建“一系列相互关联的对象”这些对象组合起来才是一个完整的产品族。举个例子一个跨平台 UI 组件库在 Windows 上有 Windows 风格的按钮、输入框、弹窗在 Mac 上有 Mac 风格的按钮、输入框、弹窗。这里“按钮、输入框、弹窗”就是一个产品族“Windows 风格”和“Mac 风格”就是两个具体的产品族。如果用工厂方法你得定义 ButtonFactory、TextFieldFactory、DialogFactory每个工厂都有 Windows 和 Mac 两个子类总共六七个类。而且调用方要分别获取这几个工厂很容易出现“用 Windows 按钮配上 Mac 输入框”的诡异组合。抽象工厂把整个产品族绑定在一个工厂里public interface UIFactory { Button createButton(); TextField createTextField(); } public class WindowsUIFactory implements UIFactory { public Button createButton() { return new WindowsButton(); } public TextField createTextField() { return new WindowsTextField(); } } public class MacUIFactory implements UIFactory { public Button createButton() { return new MacButton(); } public TextField createTextField() { return new MacTextField(); } }调用方只需要拿到一个 UIFactory要么全是 Windows 风格要么全是 Mac 风格从结构上保证产品族的一致性。抽象工厂的适用场景很清晰你的系统里存在“成套”的对象而且你必须维持成套的一致性。如果你发现自己的代码里在 new 多个关联对象而且它们之间必须配套就可以考虑抽象工厂。这里要提醒一句抽象工厂也有“开闭原则”变形的问题。新增一个产品维度比如要加一个“图标”组件就需要改动所有具体工厂的接口影响面很大。所以只有当产品族结构相对稳定时才建议用抽象工厂否则会变成噩梦。2.4 三种工厂模式如何选择很多新手在这三个模式里绕晕我提供一个简单的判断逻辑只有一个产品且创建逻辑简单用简单工厂。只有一个产品但创建逻辑复杂、种类多且经常扩展用工厂方法。有多个关联产品且需要保证成套使用用抽象工厂。从抽象程度来说简单工厂 工厂方法 抽象工厂。抽象程度越高结构越复杂灵活度也越高。项目里不是越抽象越好而是要看“变化点”在哪里。如果产品的种类会横向扩展增加新类型工厂方法合适如果产品族的维度会纵向扩展增加新成员抽象工厂会带来改动成本需要慎重。我还遇到过一种常见场景系统一开始用简单工厂后来产品类型快速增加简单工厂的 if 分支越滚越大。当时的重构方式是先引入工厂方法把每个产品的创建逻辑下沉到对应工厂子类再结合注册表模式用 Map 把产品类型和工厂类映射起来彻底消灭了 if 分支。这种演进思路比一开始就盲目上抽象工厂要务实得多。3. 单例模式从全局唯一到线程安全3.1 懒汉式、饿汉式与双重检查单例模式看起来最简单实际上坑最多。它的目标是“保证一个类只有一个实例并提供全局访问点”。最常见的两种实现是饿汉式和懒汉式。饿汉式public class ConfigManager { private static final ConfigManager INSTANCE new ConfigManager(); private ConfigManager() {} public static ConfigManager getInstance() { return INSTANCE; } }饿汉式在类加载时就创建实例线程安全但缺点是如果这个类初始化比较重而系统实际上根本没用到它就会白白浪费资源。比如某个配置管理器启动时要加载几百个配置项但只有特定模块才会用到饿汉式会让 JVM 在启动阶段就把它加载拖慢启动过程。懒汉式则把创建延迟到第一次调用 getInstance 时public class ConfigManager { private static ConfigManager instance; private ConfigManager() {} public static synchronized ConfigManager getInstance() { if (instance null) { instance new ConfigManager(); } return instance; } }这种写法线程安全了但每次调用 getInstance 都要走同步锁性能有损耗。实际上 getInstance 初始化之后同步纯粹是多余的于是出现了双重检查锁定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 在初始化完成之前不会被其他线程读取。这段代码看起来已经很完美了但我还是要说除非你明确知道自己在做什么否则不要手写这种代码。因为 volatile 和 synchronized 的语义细节很多一个不小心就是隐蔽的并发 bug。3.2 枚举单例和容器单例在 Java 面试里很多人会说“枚举单例是最佳实现”这个说法在大多数情况下是对的。枚举单例写法极其简洁public enum ConfigManager { INSTANCE; private String configPath; public void setConfigPath(String path) { this.configPath path; } }JVM 保证了枚举实例的线程安全、序列化安全还天然防反射攻击这三点往往是普通单例模式的致命伤。那为什么实际项目里枚举单例用得不多一是部分团队成员对枚举不熟悉觉得别扭二是一些老项目规范里禁止使用枚举。但如果没有任何历史包袱我强烈推荐优先考虑枚举单例。容器单例是一种更抽象的思路用一个全局 Map 管理多个单例对象对象的类型作为 key。Spring 的 IoC 容器本质上就是一个容器单例管理器。它的好处是可以用同一个套路管理成百上千个对象而不是每个类都写一遍 getInstance。容器单例适合框架层面的管理业务代码里直接用枚举或饿汉式就够了。3.3 单例模式的坑与替代方案单例模式最大的问题不是线程安全而是它掩盖了对象之间的依赖关系。调用方直接拿ConfigManager.getInstance()代码里到处都是对全局状态的隐式依赖测试的时候想换一个 fake 对象都难。如果你发现系统里几十个类都在调某个单例这个单例就是典型的“上帝对象”维护起来很痛苦。我的建议是单例模式只适合“真正全局唯一”的资源比如配置、日志、连接池。对于那些“看起来唯一但可能需要多实例”的类更推荐用依赖注入的方式管理生命周期由容器保证只创建一个实例但调用方只依赖抽象接口不依赖具体单例类。还有一种常见误区把“工具类”写成单例。纯静态方法的工具类不需要实例化也不需要保存状态写成 final class private 构造方法就够了完全不需要单例模式。单例模式是“有实例但只创建一个”静态工具类是“根本不创建实例”很多人没区分这一点。另外序列化和反射会破坏单例。常规手段是在 readResolve 方法里返回同一实例私有构造方法里加防御逻辑抛出异常。这些操作都增加代码复杂度反而印证了“能用枚举就别折腾”的建议。4. 建造者模式和原型模式4.1 建造者模式解决参数过多的构造函数建造者模式适合“参数很多且不是所有参数都是必填”的对象。经典的例子是订单对象可能有订单号、用户 ID、商品列表、优惠券、配送地址、备注、支付方式、发票信息…… 如果全塞进构造函数可能要有七八个参数调用方根本记不住顺序传错一个就是上线事故。构造器重载也不现实因为组合太多。JavaBean 的 setter 方式倒是灵活但会导致对象在创建出来后的任意时间点都处于不完整状态多线程环境下尤其危险。建造者模式把对象构建过程拆成一步步的调用最后通过 build() 一次性生成完整对象Order order new Order.Builder() .orderId(20240815001) .userId(1024L) .productList(items) .discount(coupon) .address(address) .build();这段流畅的链式调用可读性非常好而且可以用校验逻辑保证 build 时必填字段不为空。在 Kotlin 里可以简单用具名参数和默认值替代但在 Java 中建造者依然是解决参数爆炸的最实用方案。我自己的经验是如果构造函数参数超过 4 个或者有超过 3 个可选参数就可以考虑引入建造者模式。尤其是对接第三方 API 时请求对象往往有十几个可选字段用建造者模式能让调用方一眼看清设置了哪些参数避免漏配。4.2 原型模式克隆比 new 更快原型模式的思路是通过复制现有实例来创建新对象而不是通过 new。它适用于“创建对象成本高、复制对象成本低”的场景。比如一个对象需要经过复杂的数据库查询、网络请求、计算才能初始化但拿已有对象克隆一份改几个字段就行这时候原型模式能省下很多开销。Java 中的原型模式无非是实现 Cloneable 接口重写 clone 方法。但这里有个大坑Object.clone() 是浅拷贝。如果对象里有可变引用类型的字段浅拷贝会让两个对象共享同一个内部对象修改一个会影响到另一个。举个实际例子一份报表配置对象里包含一个 List过滤器列表。用浅拷贝克隆两份配置往第二份配置里加了一个过滤器第一份配置里也会多出这个过滤器而且可能引发并发修改异常。所以在重写 clone 时需要手动对 List、Map 等容器字段进行深拷贝Override public ReportConfig clone() { ReportConfig copy (ReportConfig) super.clone(); copy.filters new ArrayList(filters); return copy; }但要注意如果 List 里的元素本身也是可变对象那么光 new 一个 ArrayList 还是不够还得遍历元素逐个 clone。这就要看具体需求了深拷贝的粒度得自己控制好。4.3 克隆的正确打开方式原型模式在 Java 里用起来其实不够优雅。Cloneable 接口是个标记接口clone 方法还受 protected 限制子类想用还得抛 CloneNotSupportedException相当别扭。如果只是需要复制一个对象我更推荐用“拷贝构造函数”或“静态工厂拷贝方法”甚至在业务层用 BeanUtils 工具类的 copyProperties。另外有些场景可以用序列化实现深拷贝对象实现 Serializable通过序列化再反序列化生成一个完全独立的新对象。这种方式写起来简单但性能很差而且要求整个对象图都支持序列化如果对象里有连接池、文件句柄之类的不可序列化字段就会直接失效。所以我的实际选择是如果对象结构简单直接用拷贝构造函数一目了然。如果对象结构复杂、字段嵌套多优先考虑“原型模式 深拷贝辅助工具”比如 Apache Commons 的 SerializationUtils或者 Kotlin 的 data class 自带的 copy。不要为了用设计模式而强行实现 Cloneable那只会增加维护成本。4.4 建造者和原型模式的组合技巧在真实的复杂系统中建造者和原型模式经常一起出现。比如配置对象先通过建造者创建一份“标准配置”后续需要生成多个类似配置时直接 clone 标准配置再修改差异字段。这样既享受了建造者的可读性又享受了原型的复制效率。我在一个推荐系统项目里就干过类似的事用户请求进来要生成一个复杂的查询条件对象字段多、嵌套深创建一次成本不低。我先预置了几个常用模板模板用建造者一次性构造好后续请求来临时 clone 模板再调整筛选条件整体响应时间下降了 20% 左右。这种“模板 克隆”的模式在很多中间件代码里也有体现非常实用。5. 实战经验创建型模式在真实项目中的落地5.1 典型场景举例这里梳理几个我在项目中真实用到的创建型模式场景方便你对号入座。支付渠道对接支付渠道越来越多每种渠道的配置、签名、回调验签逻辑都不同。我用工厂方法 策略模式每种渠道一个支付处理器子类再通过工厂根据渠道码返回对应处理器新增渠道时只加一个类。报表导出导出格式有 Excel、PDF、CSV且每种格式的配置参数很多。我用抽象工厂 建造者每种格式一个工厂负责创建对应的导出器和配置对象配置对象用建造者链式赋值代码清晰很多。全局配置中心配置信息只在启动时加载一次运行中不变用饿汉式单例或者枚举单例都可以我后来改成依赖注入的方式管理生命周期方便测试时替换。App 消息通知通知内容由标题、正文、跳转链接、图片、附加参数组成字段很多且很多可选我直接用建造者模式构建消息对象调用方阅读代码比一行行 setter 舒服多了。复杂查询对象涉及多个过滤条件、排序规则、分页参数的查询对象使用原型模式 建造者做模板克隆在高并发查询场景下性能提升明显。5.2 组合使用的套路创建型模式之间不是互斥的它们经常组合使用。我常用的套路有两种。第一种是“工厂 建造者”。工厂负责根据条件决定创建哪一种具体对象而具体对象的内部结构比较复杂交给建造者拼装。比如根据用户终端类型创建不同的消息推送对象工厂拿到终端类型后调用对应的消息建造器一步步填充内容。这样工厂的职责是“选型”建造者的职责是“组装”各自单独变化互不干扰。第二种是“抽象工厂 原型”。抽象工厂负责创建一整套产品但整套产品的默认配置是预先做好的模板直接克隆再微调即可。我用这个方案做过一个多租户系统每次新租户入驻时抽象工厂根据租户类型创建权限配置、数据源配置、页面配置这些配置对象都以默认模板为原型克隆最后再覆盖租户特有字段性能和可维护性都不错。5.3 踩过的一些坑设计模式用错比不用更难受。我踩过几个典型的坑这里直接列出来。第一是滥用单例导致处处依赖。一个老的业务模块里SimpleUserService 被设计成单例里面十几个非线程安全的成员变量上线一压测就出各种奇怪的并发问题。后来把单例改成多例问题立刻消失。所以单例一定要确认“无状态”或者“状态线程安全”。第二是工厂方法导致类爆炸。一个业务场景有二十多种产品按工厂方法设计就是二十多个工厂类每个工厂类都是几行代码反而增加了项目管理成本。后来我改用注册表模式把创建逻辑用 Map 映射到产品类型代码更少也更直观。设计模式是指导不是教条。第三是建造者模式加太多默认值。有人为了让调用方“省事”给建造者里的每个字段都加了默认值结果某次漏填了一个关键业务字段系统没有报错只是用默认值处理线上出现了数据异常。我后来在 build() 方法里加了必填字段校验宁可暴露问题也不要让错误静默发生。第四是深拷贝的隐蔽坑。用 Apache BeanUtils 做深拷贝时遇到类型不一致的字段会直接抛异常而且性能也比较差。我后来逐步换成了 JSON 序列化方式或者手工编写拷贝方法虽然代码多一点但行为可控。这些都是反复试错得来的教训。设计模式本身是工具工具是为了让代码更清晰、更稳定、更易维护。如果引入一个模式反而让代码更绕、更难懂那就需要停下来想想是不是选型出了问题。5.4 创建型模式的心智模型最后分享一个我用了很久的心智模型。拿到一个创建对象的需求先看四个维度变化点对象类型会变化吗创建过程会变化吗对象结构会变化吗复杂度参数多不多初始化流程长不长唯一性允许多个实例吗成本创建成本高还是复制成本高变化点在类型优先考虑工厂系列变化点在组装过程优先考虑建造者唯一性要求强优先考虑单例创建成本高优先考虑原型。把这些维度过一遍选型基本不会跑偏。我越来越觉得设计模式的学习不在于记住 23 个 UML 图而在于形成一套“面向变化”的设计直觉。创建型模式是第一课也是你写干净代码绕不开的一课。多写、多重构、多复盘这些模式会慢慢内化成你自己的武功招式。希望这篇解析能让你在自己的项目里找到实践切入点真正用起来。