FEATURED · 精选文章

设计模式实战:单例、工厂、策略、责任链四模式全解析

发布时间 / 2026/9/13 20:13:56
来源 / 创域科博编辑部
栏目 / 资讯中心
设计模式实战:单例、工厂、策略、责任链四模式全解析 做技术这么多年我经常被问到设计模式到底该怎么学。很多新手拿着《大话设计模式》从头啃到尾合上书还是不知道代码里该用哪个。这次借着设计模式大全里最常用的单例、工厂、策略、责任链这四种模式我把项目里真正用得上的思路、写法、坑点整理成一份能直接上手的技术笔记。这篇内容适合准备面试的同学、在项目里想优化代码结构的初中级开发也适合带团队的负责人作为代码评审的依据。我不会上来就甩一堆UML图而是先用一句话说清楚每个模式到底解决了什么问题再给出可直接落地的代码示例最后聊到实际开发中我踩过的坑。先说这四种模式的共性思维它们都是在解决变化带来的问题。把不变的骨架固定下来把变化的部分抽出去封装好让代码能组合、能扩展、能测试。所以整篇笔记都可以围绕找到变化、封装变化这八个字来理解。1. 整体设计为什么是这四种模式它们解决什么问题1.1 设计模式背后的核心思想程序里的可插拔我刚学设计模式的时候有个误区以为设计模式是一堆高大上的语法技巧。直到在真实项目里重构了三次订单模块之后才转过来设计模式根本不是语法炫技而是一套对象之间怎么协作的成熟套路。程序里的耦合就像插座和插头设计模式就是把接口标准化让不同的实现像家电一样随便插拔。这四种模式放在一起刚好覆盖了面向对象设计里最常见的四类职责分配场景。单例模式解决的是一个类只能有一个实例且要全局共享的资源管理问题工厂模式解决的是客户端不该知道创建细节想创建谁就创建谁的创建解耦问题策略模式解决的是同一件事有不同的做法运行时可以自由切换的算法族替换问题责任链模式解决的是一个请求要被多个对象依次处理但发送者不必知道是谁处理的的调用方解耦问题。把它们组合起来几乎能应付后端业务开发里80%的重构需求。比如我在做支付渠道接入的时候四个模式用在了同一套代码里通过单例模式持有微信支付、支付宝支付、银联支付的客户端实例通过工厂模式根据订单金额和用户所在地区选择创建对应的支付渠道服务通过策略模式把不同的支付方式封装成统一接口切换实现只需改一行配置再把风控校验、幂等校验、签权校验串成一条责任链任何一个环节不通过请求就不会走到真正的支付下单逻辑。1.2 如何判断一个场景该用哪种模式很多人的困扰不是不知道怎么用而是不知道什么时候用。这里有一个我总结的判断模型先看你的代码里有没有变化的重心。如果你发现每次改动都要动同一个类、同一串if-else说明这里就是变化点就该拿模式去隔离开。如果变化发生在需要几个实例、实例怎么被共享考虑单例如果变化发生在到底是哪个具体对象被创建考虑工厂如果变化发生在同一个接口下一套算法互相替换考虑策略如果变化发生在多个对象都有机会处理同一个请求且处理顺序有关联考虑责任链。还有一个辅助判断看你的类名后面有没有跟太多修饰语。比如XXXManagerXXXUtilXXXService这样的类里通常混合了这四类逻辑点。遇到这种类就该想一想是不是该拆了。1.3 学习路径建议先会用再谈理解我不建议一上来就背GoF书里那23个模式的UML图。比较有效的路径是先拿一段已经被if-else糊住的烂代码用某个模式重构成干净的版本感受一下重构前后的差别。这也就是为什么我把这四种模式按照从最常用到最具结构性的顺序来写读者可以先从单例这种最不复杂的切入用手感建立信心再往责任链这种链条装配的模式深入。另外建议在学习时配合一个真实的业务场景。比如用下单支付流程把这四种模式都套进去练习比毫无上下文地写Demo有用得多。下面进入正文逐一说清每种模式。2. 单例模式从懒汉到枚举线程安全的完整路线2.1 单例模式的本质全局唯一实例的门禁单例模式看起来很简单类图就一个类私有构造方法提供一个静态方法全局访问。我见过不少人觉得好像就是静态变量静态方法而已甚至直接写了一个全局对象就没再用单例。但单例的真正价值在于它把是否只有一个实例这个约束封装在类内部而不是靠约定、靠文档提醒大家都别new。场景最有代表性的就是配置管理器、数据库连接池、线程池、日志管理器、Spring容器里的默认Bean。这些对象如果被new出多份要么是资源被浪费要么是数据一致性直接被破坏。比如日志管理器被多次实例化不同地方打印的日志可能写到不同的文件句柄排查问题的时候会发现日志缺了一段非常头疼。2.2 懒汉与饿汉最常见的两派写法单例实现里最经典的两种写法我直接给出Java版本对比。饿汉式public class EagerSingleton { // 类加载时即创建天然线程安全 private static final EagerSingleton INSTANCE new EagerSingleton(); private EagerSingleton() { // 防止外部通过构造器创建实例 } public static EagerSingleton getInstance() { return INSTANCE; } }懒汉式线程安全的双重检查锁版本public class DoubleCheckSingleton { // volatile保证多线程下的可见性和禁止指令重排序 private static volatile DoubleCheckSingleton instance; private DoubleCheckSingleton() { } public static DoubleCheckSingleton getInstance() { if (instance null) { // 第一次检查减少进入同步块的次数 synchronized (DoubleCheckSingleton.class) { if (instance null) { // 第二次检查防止多个线程同时创建 instance new DoubleCheckSingleton(); } } } return instance; } }饿汉式以牺牲启动时间为代价换取线程安全类一旦加载就会初始化实例不存在多线程竞争的问题。但如果有类被加载后一直不被使用实例却被提前创建了等于占着资源不用。懒汉式的双重检查锁是面试高频题关键是那两次if和volatile缺一不可。如果省掉volatile在极端情况下线程A执行完new操作但还没写入引用字段前线程B可能读到一个半初始化的对象。有人会问加synchronized不就行了为什么还要volatile因为synchronized保证的是互斥访问而volatile在这里保证的是可见性和禁止重排序。new操作在JVM里面至少有三步分配内存、调用构造器初始化、把引用指向该内存。如果没有volatile第二步和第三步可能被重排序线程B拿到引用的时候对象可能还没构造完直接使用会出问题。2.3 更优雅的实现静态内部类和枚举在Java里还有两种写法我推荐在项目里优先考虑。第一种是静态内部类public class InnerClassSingleton { private InnerClassSingleton() { } private static class Holder { private static final InnerClassSingleton INSTANCE new InnerClassSingleton(); } public static InnerClassSingleton getInstance() { return Holder.INSTANCE; } }这种方式利用类加载机制保证懒加载和线程安全只有当getInstance()被调用时静态内部类才被加载实例才被创建。写起来简洁性能也行没有同步开销。第二种是枚举public enum EnumSingleton { INSTANCE; public void doSomething() { // 业务方法 } }用枚举实现单例可以说是终极大招。它天然防反射攻击、防序列化破坏。普通的单例类通过反射你可以把构造方法设置成可访问然后new出第二个实例实现Serializable接口的话反序列化时也可能得到一个新的实例。而枚举类型在JVM层面就保证了唯一性这也是Effective Java作者Joshua Bloch反复推荐的方式。2.4 C场景下的线程安全单例热词里出现了c 单例模式 线程安全因为C的情况与Java有些不同。C11之前实现线程安全单例非常麻烦。C11之后有了魔性静态变量初始化机制可以在局部静态变量的首次初始化时由编译器保证线程安全于是Meyers Singleton成为C下最推荐的写法class Singleton { public: static Singleton getInstance() { static Singleton instance; // C11之后线程安全的懒汉式 return instance; } Singleton(const Singleton) delete; Singleton operator(const Singleton) delete; private: Singleton() default; };这里把一个静态局部变量放在函数体内C11标准规定多个线程同时首次调用这个函数时局部静态变量的初始化只发生一次而且是线程安全的。这是最简洁、性能最好的方案。注意C里还有一个坑是单例的析构顺序特别是多个单例互相引用时析构顺序不确定会导致崩溃。所以建议单例尽量少直接依赖其他单例能传参就传参。2.5 单例模式的实际注意事项在实际项目中用单例我总结了几条经验第一单例不推荐放进接口或作为参数传递。单例本身就是全局的如果到处传来传去反而模糊了它全局唯一的本质也不利于测试替换。第二如果你是写测试的单例是一个让人头疼的东西。因为单例一旦创建就很难被重新初始化。我在测试配置类的时候会用反射或者专门的test hook去重置实例麻烦是麻烦了点但总比每次跑用例都要重启JVM强。第三很多人误以为全局变量就是单例。这是错误的。全局变量只是能用但没法防止别人再new一份也没法保证初始化时机。单例的价值在于强制唯一性而全局变量只是碰巧唯一。3. 工厂模式从简单工厂到抽象工厂一次说完三种形态3.1 工厂模式解决什么问题别让client直接new工厂模式的核心可以浓缩成一句话创建对象的过程不能散落在客户端代码里。为什么因为一旦创建过程包含复杂的参数组装、配置加载、依赖初始化等等每次new都是在重复这段逻辑一旦创建方式发生变化比如加了参数或者换了实现类所有调用点都要跟着改。打个比方你去餐厅吃饭不需要知道后厨怎么洗菜、切菜、配调料你只要对服务员说来一份宫保鸡丁后厨自然会按流程做好端出来。客户端代码就应该是这个点菜的人只说需求不碰创建细节。这里需要注意热词里还有一个海信液晶电视怎么进工厂模式这跟设计模式完全是两码事。电视的工厂模式是电视系统里用于调试和工程测试的隐藏菜单入口不是我们代码里说的工厂模式大家别把两者的概念混淆了。本文提到的工厂模式都是面向对象设计里创建对象的那一系列手法。3.2 简单工厂最入门但违背开闭原则先看一段最常见的简单工厂写法public class PaymentFactory { public static Payment create(String channel) { if (wechat.equals(channel)) { return new WechatPay(); } else if (alipay.equals(channel)) { return new Alipay(); } else if (unionpay.equals(channel)) { return new UnionPay(); } throw new IllegalArgumentException(未知支付渠道: channel); } }简单工厂不是GoF 23种设计模式之一但它非常普及因为它最简单。用一个静态方法通过参数返回不同的产品实例。问题也很明显如果新增一个支付渠道就要改这个静态方法违背了开闭原则对扩展开放对修改关闭。如果渠道少、变化不频繁简单工厂完全够用。但一旦渠道数量膨胀到10个以上这个工厂方法就会变成又长又臭的if-else链。3.3 工厂方法模式把工厂抽象化各产品各配一家工厂工厂方法模式的思路是把工厂这个概念抽象出来每种产品对应一个具体的工厂类public interface PaymentFactory { Payment create(); } public class WechatPayFactory implements PaymentFactory { Override public Payment create() { // 微信支付特有初始化比如配置appId、商户号 return new WechatPay(); } } public class AlipayFactory implements PaymentFactory { Override public Payment create() { // 支付宝特有初始化比如配置应用ID、私钥 return new Alipay(); } }客户端只需要依赖PaymentFactory接口在配置里指定具体使用哪个工厂或通过依赖注入把工厂实例传进来。新增渠道时新增一个XxxPayFactory类就好老的工厂类一个不用动。这里的关键变化简单工厂把选择分支放在工厂里面工厂方法模式把选择分支交给了客户端的装配。客户端仍然需要通过配置文件或ApplicationContext来决定用哪家工厂但是创建逻辑本身被封死在各家工厂里了。3.4 抽象工厂模式围绕产品族的一致性好帮手抽象工厂模式比工厂方法模式再进一步但不建议一上来就强上。简单来说工厂方法模式的每个工厂创建一种产品抽象工厂模式的每个工厂要保证同一系列的产品能配套生产。我用一个更贴近实际的后端场景来解释。假设你在做一个跨数据库的ORM配置模块需要同时支持MySQL和PostgreSQL。每个数据库都有三种对象连接池、方言(Dialect)、类型转换器(DataMapper)。为了保证不出现MySQL连接池配了PostgreSQL的方言这种混乱就可以引入抽象工厂public interface DatabaseFactory { ConnectionPool createConnectionPool(); SqlDialect createSqlDialect(); ResultSetMapper createResultSetMapper(); } public class MySqlDatabaseFactory implements DatabaseFactory { Override public ConnectionPool createConnectionPool() { return new MySqlConnectionPool(); } Override public SqlDialect createSqlDialect() { return new MySqlDialect(); } Override public ResultSetMapper createResultSetMapper() { return new MySqlMapper(); } }抽象工厂最大的特点是族约束。通过接口的返回类型编译器就能保证同一系列的产品一起出现不会出现组合错乱。代价是接口一旦设计完成要支持新的产品维度就非常困难。比如上面这个DatabaseFactory想加一个TransactionManager所有实现类都得改。所以抽象工厂适用于产品族比较稳定的场景新增一个产品族容易新增一类产品维度难。3.5 工厂模式的代码组织技巧最后补充一些工厂模式落地时的经验。不要接过多的初始化参数。我见过有人把工厂方法设计成十几二十个参数谁调用谁崩溃。更好的方式是把参数收拢成一个Config对象或者是工厂根据环境变量/配置中心动态决定参数。工厂类的职责要单一创建完对象就返回不要在工厂里顺手做业务校验。工厂里做业务校验会让工厂对自己生产出来的产品有过多隐性的假设将来换实现的时候容易发现校验逻辑对不上。如果项目用的是Spring其实Spring的Bean方法本身就是一个工厂。在配置类里声明一个个Bean方法返回统一接口类型实际返回哪种实现由配置或条件注解决定。这比手写Factory类要省事得多推荐优先考虑。4. 策略模式干掉项目中90%的if-else4.1 策略模式的核心思想算法可以作为参数传递策略模式是我在代码评审时最喜欢推荐给团队的一个模式因为它对代码可读性的提升极其明显。它的核心思想是把某个算法或行为封装成独立的策略对象使它们可以互相替换且替换行为不影响客户端。生活化地理解就好比旅游。去同一个地方你可以坐飞机、坐火车、自驾。目的地一样但出行方式可以随时切换而且你不需要改动旅游这个流程本身只需要在出发前选一个交通工具。策略模式就是把旅游流程和被选中交通工具解耦了。4.2 一个促销活动里的策略模式示例假设你有电商项目商品有满减、折扣、无优惠三种价格计算规则。不用策略模式时一堆if-else如下public BigDecimal calculatePrice(String promotionType, BigDecimal amount) { if (DISCOUNT.equals(promotionType)) { return amount.multiply(new BigDecimal(0.8)); } else if (FULL_REDUCTION.equals(promotionType)) { if (amount.compareTo(new BigDecimal(100)) 0) { return amount.subtract(new BigDecimal(20)); } return amount; } else if (NONE.equals(promotionType)) { return amount; } throw new IllegalArgumentException(未知促销类型); }一旦新增一种满100减20再打九折的玩法这个方法的if-else又会增加一条。测试时也要把所有可能性完整过一遍。用策略模式重构之后public interface PriceStrategy { BigDecimal calculate(BigDecimal amount); String type(); } Component public class DiscountStrategy implements PriceStrategy { Override public BigDecimal calculate(BigDecimal amount) { return amount.multiply(new BigDecimal(0.8)); } Override public String type() { return DISCOUNT; } } Component public class FullReductionStrategy implements PriceStrategy { Override public BigDecimal calculate(BigDecimal amount) { if (amount.compareTo(new BigDecimal(100)) 0) { return amount.subtract(new BigDecimal(20)); } return amount; } Override public String type() { return FULL_REDUCTION; } }然后在促销上下文里把策略收拢起来Service public class PriceCalculator { private final MapString, PriceStrategy strategyMap; public PriceCalculator(ListPriceStrategy strategies) { strategyMap strategies.stream() .collect(Collectors.toMap(PriceStrategy::type, Function.identity())); } public BigDecimal calculate(String promotionType, BigDecimal amount) { PriceStrategy strategy strategyMap.get(promotionType); if (strategy null) { throw new IllegalArgumentException(未知促销类型: promotionType); } return strategy.calculate(amount); } }这段代码里使用Spring的依赖注入把所有PriceStrategy实现类收集到一个列表里再转成Map。新增一种促销策略只需要写一个新的Component实现类不用改任何现有业务代码完全符合开闭原则。4.3 策略模式与状态模式的区别很多人分不清策略模式和状态模式这两个名字听起来都很像换一个对象解决不同情况。关键区别在于谁决定切换。策略模式下客户端主动选择策略策略对象不知道其它策略的存在策略也不关系自己什么时候被调用。状态模式下状态之间也会互相切换状态对象知道自己执行结束后下一个状态是谁切换不由外部主导而是由状态内部转移。举个例子订单状态流转走到已支付状态时自动推进到已发货状态这是状态模式。而客户在这个接口里选择微信支付还是支付宝支付这是策略模式。4.4 策略模式适用场景和局限性策略模式特别适合的情况一个接口有多个实现、大家的行为互不相干、运行时要动态选。日志记录格式切换、支付渠道切换、排序规则封装、校验规则集合打包这些我都用过。策略模式也有它的限度不要为了消掉if-else而强行上策略。如果分支只有两三个且不太可能扩展直接用二元运算符或简单的if也完全可以过度设计比if-else更糟糕。另外策略类如果数量过多比如几十个管理成本会上升。这时候可以再引入工厂模式把策略创建和选择逻辑统一在一个工厂里面让业务代码只面对一个策略入口。5. 责任链模式把请求交到一条流水线上5.1 责任链模式的核心思想每个环节有各自的分工责任链模式把多个处理对象串成一条链请求沿着链向后传递直到被某个对象处理或者完全通过。它适合处理系统的验证拦截类逻辑比如登录校验、参数校验、权限校验、风控审核、多级审批。我最早理解这个模式是通过请假流程员工提交请假申请先由组长审批如果请假天数小于3天组长批了就结束了超过3天还要经理审批超过10天还要VP审批。这个流程就是一条典型的责任链申请人根本不需要知道自己到底会走到哪一级他只需要把请假单递出去。5.2 链的硬编码实现一个最直接的责任链实现每个Handler内部持有对下一个Handler的引用处理完自己职责后决定是交给下一个还是结束流程。public abstract class Approver { protected Approver next; public void setNext(Approver next) { this.next next; } public abstract void approve(Request request); } public class LeaderApprover extends Approver { Override public void approve(Request request) { if (request.getDays() 3) { System.out.println(组长审批通过); } else if (next ! null) { next.approve(request); } } }这种写法直截了当但缺点是链的组装散落在客户端代码里每次调用都需要把LeaderApprover、ManagerApprover、VpApprover挨个setNext串起来。如果团队里不同模块对链的要求不一样组装代码将面临大量复制粘贴。5.3 更灵活的实现把链的装配交给容器在Spring环境里我通常会换一种写法用一个处理器列表代替手写next引用通过Order注解控制执行顺序。这样做的好处是把链的拼接从手写setNext变成框架自动装配。public interface ApproverHandler { void handle(RequestContext context); } Component Order(1) public class LeaderHandler implements ApproverHandler { Override public void handle(RequestContext context) { if (context.getDays() 3) { context.setApproved(true); System.out.println(组长审批通过); } } } Component Order(2) public class ManagerHandler implements ApproverHandler { Override public void handle(RequestContext context) { if (!context.isApproved() context.getDays() 10) { context.setApproved(true); System.out.println(经理审批通过); } } }组装逻辑由Spring容器把列表注入进来业务代码只需要循环调用public class ApproverChain { private final ListApproverHandler handlers; public ApproverChain(ListApproverHandler handlers) { this.handlers handlers; } public void execute(RequestContext context) { for (ApproverHandler handler : handlers) { handler.handle(context); if (context.isApproved()) { break; } } } }这种实现和重写ArrayList类似用一个列表模拟链表好处是顺序很容易调整只要修改Order注解的值就行。坏处是没法做太复杂的链终止条件只能是通过就break。不过大多数业务场景这样已经够用。5.4 责任链在框架层面的应用Filter、Interceptor和中间件在Java Web框架里责任链模式早就是底层基础设施了。Servlet的Filter、SpringMVC的Interceptor、Netty的ChannelPipeline都长着责任链的脸。我在排查线上一个问题时突然意识到一个HTTP请求从进入Tomcat到被Controller处理中间可能会经过身份认证Filter、参数解析Filter、日志Filter、限流Filter等等这就是一条活生生的责任链。所以学责任链模式的好处不只是用来应付面试题更重要的是你能看懂框架源码里那层又一层配置类到底在做什么。当你理解了Handler、FilterChain这些概念之后自己写中间件、写插件系统时就会非常顺手。5.5 责任链模式要注意的坑最大的坑是链变得太深太长。链每增加一环请求的耗时就会增加调试时也要多跳几步。我建议把职责相似的处理逻辑合并成一个Handler不要为了追求链的完整而硬拆。第二个坑是异常处理。责任链模式天然适合顺序执行但如果某一个环节抛异常后面的环节就执行不到了。有些场景这没问题比如参数校验类有些场景需要即使前面失败后面也要记录日志比如风控统计这时就要做成catch-and-continue或者用不同的接口拆分出校验型Handler和统计型Handler。第三个坑是难以定位问题。链路一旦很长某个请求在哪里被拦截、在哪里被篡改排查起来都麻烦。我的习惯是在每个Handler的入口和出口打一行日志打印请求ID方便跟踪链路。这也是为什么很多框架里都有调用链中间件的原因。6. 四种模式如何选型一张表说清使用边界6.1 模式对比速查表写到最后我整理一张速查表方便后续做技术选型时直接参考模式核心解决维度典型场景核心缺点常用实现语言单例模式实例唯一配置管理器、连接池、线程池单例生命周期与测试耦合Java/C/Python工厂模式创建过程解耦支付渠道创建、数据库方言创建类数量暴增Java/C/Python策略模式算法族切换价格计算、排序规则、格式封装策略过多时管理成本高Java/Python责任链模式请求多级处理校验、审批、过滤器、中间件链路过长难调优Java/Python这张表把四种模式放在一起看重心的区别很直观。单例是一个工厂是创建策略是可替换算法责任链是依次处理。实际项目里这四种往往组合出现比如工厂创建出来的对象内部又使用了策略模式策略的选择器又在责任链里被调用这些都是正常的设计模式本来就是积木可以互相嵌套。6.2 组合使用的实例参考不妨设想一个商品下单场景单例的库存服务控制全局库存扣减并发安全工厂根据商品类目创建不同的计价策略计价策略中的折扣逻辑通过策略模式支撑不同会员等级扣减库存前的一连串前置校验封装成责任链。这样四个模式各司其职代码逻辑非常清晰。6.3 不要过度设计的忠告前面说了很多模式的好处这里我得泼一盆冷水。设计模式是用来解决复杂度升级带来的维护问题的但别为了用模式而用模式。如果你当前页面只有三五百行代码需求也不会快速膨胀直接写顺序逻辑可能是最清晰的选择。真要抽象出十几个接口、十几个类反而增加阅读成本。我在评审代码时有一条标准如果新来的开发需要花超过30分钟才能理解你为什么要拆出这么多类那就说明设计过度了。优秀的设计是这个度让经验丰富的工程师觉得清晰让新人花10分钟能懂让改动需求时只动一个地方。这比学一套完美模式更有价值。6.4 最后一句话的心得用了这么多年设计模式我最大的体会是不要把它当成面试题库去背要把它当成代码坏味道的治疗方案去理解。看到if-else太长就想想策略看到到处new同一个对象就想想工厂看到要求全局唯一就想想单例看到一段请求要被反复过滤就想想责任链。描述代码遇到的问题然后找一个对症下药的模式你就能自然记住这些设计思路不需要死记硬背。另外如果你在跟踪相关热词时看到大话设计模式这类书建议看完每章后亲手用一个真实业务场景重写一遍只在项目代码里跑过一遍的模式才真正拿到生产环境里能用。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻