1. 从Java到Spring:工厂模式的前世今生
很多同学在学Spring的时候,都卡在“工厂模式”这一步。学之前觉得它就是个简单的创建对象的方式而已,学完之后发现到处都有它的影子——BeanFactory、ApplicationContext、FactoryBean,还有个叫ObjectFactory的东西,名字长得差不多,作用又各不相同。今天这篇笔记,我尽量把自己的理解一条条捋清楚,不绕弯子,也不硬凹专业术语,只讲这东西在Spring里到底是怎么“生存”的。
先给没接触过工厂模式的读者一个直观概念:假设你是个顾客,去一家餐厅吃饭。你不需要关心厨师怎么备菜、灶台怎么开火、佐料怎么放,你只需要看菜单点菜,服务员会把做好的成品端上来。在这个类比里,餐厅就是“工厂”,服务员就是“工厂方法”,你点菜的过程就是“调用工厂接口”,端上来的菜就是“实例对象”。你从始至终不知道厨师是谁、后厨长什么样,也完全不需要知道。这其实就是工厂模式的核心——调用者与创建者解耦。
引入到Java里,如果你的代码里到处都是new Something(),那么一旦某个类的构造方法签名变了,或者它的创建逻辑复杂了,你就得满世界去改调用点。工厂模式就是把这些“创建细节”集中收编到一个地方统一管辖。
Spring框架本身就是工厂模式最极致的应用。我们平常写的ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"),这个ApplicationContext的底层,就是一个超级工厂。它不是帮你造一杯咖啡,而是帮你创建和管理一组完整的、互相依赖的对象(我们称之为Bean)。这背后是“控制反转”的思想——对象不再自己去new依赖的对象,而是由Spring容器这个“大工厂”统一装配、注入。
那这篇笔记会覆盖些什么东西呢?首先是工厂模式的基本形态:简单工厂、工厂方法、抽象工厂;然后重点看Spring里的BeanFactory和ApplicationContext这两个核心接口怎么实现工厂思想;接着讲FactoryBean——这是很多人容易和BeanFactory搞混的兄弟概念;再聊聊ObjectFactory和Provider在解决依赖延迟时的实战用法;最后结合代码案例,把工厂模式在Spring配置层面的使用串起来。无论是初学者还是写了一段时间Spring但概念模糊的同学,这篇文章都能帮你把这块知识补齐。
2. 三个经典工厂模式版本:你真的搞懂区别了吗
2.1 简单工厂:一个“大管家”统筹创建
先从最基础的说起。假设要创建不同类型的数据库连接池,有DruidPool、HikariPool、C3P0Pool,它们都实现了同一个接口ConnectionPool。最常见的写法是这样:
public class PoolFactory { public static ConnectionPool create(String type) { if ("druid".equals(type)) { return new DruidPool(); } else if ("hikari".equals(type)) { return new HikariPool(); } else if ("c3p0".equals(type)) { return new C3P0Pool(); } throw new IllegalArgumentException("Unknown pool type: " + type); } }这种形态就叫简单工厂,它的关键特征是把创建逻辑集中在一个静态方法里,客户端只需要传一个type参数拿到对应的对象。看起来很方便,但它的缺点也很明显:每新增一种池类型,就必须修改PoolFactory里的if-else分支。这违反了“开闭原则”(对扩展开放、对修改关闭)。在生产环境中,频繁改既有代码意味着回归测试面变大,出问题的概率也随之上升。
不过应用场景里也不是完全不能用。如果产品线的类型比较稳定,短时间内不会频繁扩容,简单工厂反而最直观最省事。像一些内部小工具类,用简单工厂完全没问题,追求过度设计反而适得其反。
2.2 工厂方法:把创建动作下放给子类
既然简单工厂只是把If-else搬了个家,那工厂方法模式更进一步——每个具体产品对应一个具体的工厂类,大家继承同一个抽象工厂接口。
public interface PoolFactory { ConnectionPool create(); } public class DruidPoolFactory implements PoolFactory { @Override public ConnectionPool create() { return new DruidPool(); } } public class HikariPoolFactory implements PoolFactory { @Override public ConnectionPool create() { return new HikariPool(); } }调用方持有的类型是PoolFactory,具体的实现是DruidPoolFactory还是HikariPoolFactory,可以在运行时结合配置或参数去决定。对比简单工厂,它的优势在于新增类型时不需要修改已有代码,只需要新增一个工厂子类和对应的产品类。相对应的,类的数量会变多,系统结构会显得更“重”一些。工厂方法模式在项目里常见于封装各类第三方SDK的客户端:比如对接多家支付渠道,每个渠道一套实现,各自维护各自的初始化逻辑。
2.3 抽象工厂:解决“产品族”问题
如果说工厂方法关注的是“一种产品怎么创建”,那抽象工厂关注的是“一组有关的、相互关联的产品怎么统一创建”。还是拿连接池举例,一个完整的连接池可能需要配套连接校验器、统计采集器、告警器等组件。抽象工厂会一次性定义出一整套接口方法:
public interface PoolComponentsFactory { ConnectionPool createPool(); Validator createValidator(); MetricsCollector createMetricsCollector(); }每个具体的工厂实现负责生成一套风格一致的组件,比如生产环境的池方案配一套生产级监控,测试环境配一套轻量级模拟监控。这么做最大的好处是保证同一产品族内部的一致性——你永远不会出现连接池是Druid但监控组件却是配套C3P0的情况。
在Spring源码里,你其实能看到大量这种分层设计的思想。比如BeanFactory这个顶级接口,只定义了getBean这类最基础的能力,往下扩展出HierarchicalBeanFactory(支持父子容器)、ListableBeanFactory(支持批量列举)、ConfigurableBeanFactory(支持配置和生命周期管理)等。这本质上就是一套高大上的接口分级体系,让不同阶段、不同场景的调用方只面对自己所需的“视角”。学到这里,可以给自己提个问题:Spring到底是简单工厂还是抽象工厂?后面我会给出自己的理解。
3. 进入Spring6:BeanFactory和ApplicationContext到底怎么运作
3.1 BeanFactory:所有IoC容器的“老祖宗”
BeanFactory是Spring IoC容器的底层根接口。任何一个Spring容器,本质上都是在实现BeanFactory的能力。它定义了最核心的getBean(String name)、getBean(Class<T> requiredType)等方法,调用方只需要告诉容器“我要什么”,容器就负责把对应的Bean创建出来并返回。
那Spring的BeanFactory和我们自己写的PoolFactory有什么本质区别呢?最大的一点是通用性和反射机制。自己写工厂,每种产品都要写对应的if-else或子类;Spring的工厂本身不关心Bean的类型到底是什么,它通过BeanDefinition(Bean的定义信息)来知道“这个Bean的类型是谁、构造参数是什么、依赖哪些其他Bean”,然后利用反射来实例化。换句话讲,你只要在配置文件或配置类里声明好元数据,工厂不需要改代码就能创建任意类型的对象。
为了更直观地说明这一点,我画个非常简化的流程:
- 容器启动时解析配置文件(或扫描注解),生成一个或多个
BeanDefinition对象。 BeanDefinitionRegistry把BeanDefinition注册到容器内部。- 当调用
getBean时,DefaultListableBeanFactory根据BeanName找到对应的BeanDefinition。 - 实例化策略(如
SimpleInstantiationStrategy、CglibSubclassingInstantiationStrategy)反射创建实例。 - 根据
BeanDefinition中的属性值和依赖配置进行填充(依赖注入)。 - 执行初始化方法,返回最终Bean。
我们平时写的applicationContext.getBean("userService"),表面上直接拿到了对象,但背后发生了一整套复杂的流程。这套流程被封装在BeanFactory的各个子实现类中,调用方完全无感。这就是工厂模式“封装创建细节”这一定义在Spring里最精华的体现。
3.2 ApplicationContext:站在巨人肩膀上的增强型工厂
虽然BeanFactory定义了最基本的能力,但直接在项目中用BeanFactory作为容器的场景其实很少。原因很简单——它太“素”了。真实的生产环境还需要国际化支持、事件发布、AOP增强、资源加载等能力,这些统统不是BeanFactory的职责范围。
于是ApplicationContext出场了。它本身继承了ListableBeanFactory和HierarchicalBeanFactory,同时扩展出了ApplicationEventPublisher(事件发布)、ResourcePatternResolver(资源解析)、MessageSource(国际化)等接口。你可以这么理解:ApplicationContext是在BeanFactory之上做了大量功能增强的“高级工厂版本”。
项目里常见的ClassPathXmlApplicationContext、AnnotationConfigApplicationContext都是ApplicationContext的实现类。前者从classpath下的XML配置文件加载上下文,后者以注解配置类为入口。无论哪种方式,它们最终都会刷新容器、注册配置类、实例化所有单例Bean、发布容器刷新事件。日常开发中,我们几乎只跟ApplicationContext打交道,而BeanFactory更像是隐藏在背后的“基建”。
为了加深理解,做一个小测试。看下面这段代码:
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); UserService userService = context.getBean(UserService.class);当我们把ApplicationContext当成工厂来理解时,整个调用链就变成——调用方通过context.getBean()向工厂要一个UserService对象,工厂内部通过前面的六步流程组装好Bean,再交给调用方。UserService到底是怎么new出来的、它依赖的UserDao是从哪来的,调用方一概不知。这正是工厂模式追求的效果。
3.3 不同容器的创建机制差异
很多人会在面试或者实际调试中遇到一个问题:AnnotationConfigApplicationContext和ClassPathXmlApplicationContext创建Bean的方式有什么区别?其实区别不在“创建Bean”这一步,而在“创建BeanDefinition”这一步。
AnnotationConfigApplicationContext通过扫描@Component注解、解析@Bean方法来生成BeanDefinition。ClassPathXmlApplicationContext通过解析XML文档中的<bean>标签来生成BeanDefinition。GenericApplicationContext更灵活,可以在运行时手动注册BeanDefinition。
一旦BeanDefinition生成完毕,后续的实例化和初始化流程基本是统一调度。可以类比为:不管你是用手机App点餐还是在收银台人工下单,后厨做菜的方式是同一套。工厂模式设计的关键,就是把“产品如何定义”和“产品如何生产”两件事拆开,前者的差异在配置解析层收敛,后者的差异在实例化层隔离。
如果你用Spring Boot,通常只需要配合@SpringBootApplication就能启动整个应用,那是因为SpringApplication在背后帮你创建了AnnotationConfigApplicationContext(或者ServletWebServerApplicationContext等),然后自动完成配置加载和Web服务器启动。Spring Boot的“自动配置”思路,也可以理解成——框架把创建和组装过程进一步封装,开发者的配置量被压到了最低。
4. FactoryBean:最容易和BeanFactory混淆的实战概念
4.1 FactoryBean的价值场景
FactoryBean是我当年学习Spring时最容易卡壳的一个点。BeanFactory和FactoryBean,一个叫“Bean工厂”,一个叫“工厂Bean”,名字看起来就是换了个词序,实际上是两个完全不同的东西。简单讲,BeanFactory是顶级容器,负责管理一切;FactoryBean是一种特殊的Bean,它自己本身也是一个Bean,但它同时具备“生产其他对象”的能力。
什么情况下会用到FactoryBean呢?最典型的场景是:你要创建的Bean不是一个简单的类对象,而是一套复杂的组装产物。比如集成MyBatis框架时,每个Mapper接口的动态代理对象生产逻辑都是相似的:做配置解析、加载SQL映射、生成动态代理。这时候如果每个Mapper都走一遍普通Bean实例化流程,配置量会爆炸。SqlSessionFactoryBean就是专门用来解决这个问题的。它实现FactoryBean接口后,Spring在需要SqlSessionFactory时会调用其getObject()方法,由这个工厂Bean统一创建SqlSessionFactory实例。
public class MyServiceFactoryBean implements FactoryBean<MyService> { @Override public MyService getObject() throws Exception { // 在这里集中封装复杂创建逻辑 MyService service = new MyService(); service.setName("custom-name"); service.init(); return service; } @Override public Class<?> getObjectType() { return MyService.class; } @Override public boolean isSingleton() { return true; } }4.2 getBean返回的是什么?关键细节别搞错
如果注册了一个FactoryBean<MyService>到Spring容器,那么调用context.getBean("myServiceFactoryBean"),返回的到底是MyServiceFactoryBean本身,还是MyService对象?这里就是最容易踩坑的地方:
- 按Bean名称获取时,如果该Bean实现了
FactoryBean接口,默认优先返回的是FactoryBean生产的对象(即getObject()的返回值)。 - 如果想拿
FactoryBean本身,需要在名称前加一个&前缀,比如context.getBean("&myServiceFactoryBean")。
这个设计和JNDI里的java:comp/env名称解析方式一脉相承。加&约定是为了区分“工厂对象”和“产品对象”,避免两个都已经成为容器中“可用对象”的情况下出现歧义。
还有一个细节是isSingleton()方法。它决定getObject()产出的对象是否作为单例缓存。如果返回false,则每次getBean都会调用getObject()重新生成一个对象。在写自定义FactoryBean时,这一点需要格外留意,不然会出现预期单例却越拿越多的Bug。
4.3 自定义FactoryBean的注意事项
我自己在项目中写自定义FactoryBean时,踩过几个很有代表性的坑。如果你是首次动手,建议着重关注这三个:
- 小心循环依赖。
FactoryBean本身也可能被Spring容器当作一个Bean来管理,如果它又要依赖别的Bean,而别的Bean又反过来依赖这个FactoryBean,就可能出现创建顺序难控的连锁问题。 getObjectType()不要随意返回null。很多类型匹配逻辑(比如按类型拿Bean、自动装配候选人筛选)都依赖这个返回值,返回null会让框架无法准确识别这个工厂的产品类型。getObject()里不要注入自己所在容器的ApplicationContext。如果非要使用容器资源,尽量通过ApplicationContextAware接口拿到上下文引用后,再谨慎使用getBean,避免引发额外的初始化分支。
实践证明,FactoryBean在屏蔽复杂创建逻辑、动态代理生成、有参构造策略等场景非常好用。尤其是当你发现某个Bean的创建过程需要组装很多组件、做很多初始化动作时,FactoryBean往往比在@Bean方法里写一大堆代码更合适——因为你可以复用这套工厂逻辑到多个Bean上,抽象程度更高。
5. ObjectFactory与Provider:延迟依赖到底解决了什么问题
5.1 ObjectFactory的基本用法
在Spring里,如果一个Bean A依赖另一个Bean B,默认情况下容器在创建A的同时会创建B(如果B是单例且不是懒加载)。但有些场景我们不希望这么“急切”——比如B是一个连接池或者缓存的初始化成本比较高,A只有在某种特定操作下才真正需要B。这时候如果还在初始化阶段就把B创建出来,就会白白增加开销。
ObjectFactory<T>接口把这种“延迟获取”变成了一种标准姿态:
@Component public class OrderService { @Autowired private ObjectFactory<OrderValidator> validatorFactory; public void processOrder(Order order) { // 只有在执行到这里时,才真正去容器中查找并创建OrderValidator OrderValidator validator = validatorFactory.getObject(); validator.validate(order); // 业务逻辑继续... } }注意,OrderService本身不直接持有OrderValidator,它持有的是一个“供应商”角色。当调用getObject()时,才触发容器的Bean查找流程。这个设计的好处有几个:启动时不强制完整初始化所有依赖;运行时不只是因为用到的时刻才创建;还能有效绕开一部分循环依赖问题(因为依赖不是构造函数需要的即时引用,而是延迟获取)。
5.2 ObjectFactory与Provider的异同
Spring在后续版本中还引入了标准javax.inject.Provider<T>接口(在Jakarta规范中是jakarta.inject.Provider<T>)。二者在常规使用上非常相似:都是延迟获取Bean的手段。最大的区别是ObjectFactory是Spring自己定义的类型,和Spring生态绑定得更紧密;Provider则是Java依赖注入标准(CDI)中定义的接口,具备更强的规范性,在脱离Spring的架构里也能沿用同一套思想。
从使用角度,如果项目本身不打算依赖额外标准,直接用ObjectFactory最省事,源码里自带,不需要额外导入。如果团队偏向统一技术规范,或者在多框架异构环境中考虑代码的可移植性,用Provider会更稳妥。
5.3 ObjectFactory在Spring源码中的著名用法
ObjectFactory最经典的曝光场景,其实藏在循环依赖的解决机制里。Spring在创建单例Bean时,为了避免A依赖B、B又依赖A这种相互引用的尴尬,会提前暴露一个ObjectFactory,通过它对外提供一个“半成品Bean的引用”。一旦后续发现B需要注入A时,就把这个提前暴露的引用注入进去,从而绕开顺序问题。
这个设计思路非常巧妙,从工厂模式的视角看,它也是一个“延迟到关键时刻再做解析”的典型应用。如果不依赖这个机制,Spring的设计就会被迫通过多级缓存或者代理来解决,复杂度会直线上升。理解了ObjectFactory的延迟获取价值,也就理解了Spring单例池三级缓存里的一部分秘密。
5.4 实战建议:什么时候用延迟依赖
我个人的经验判断是:如果依赖的对象满足下面三个特征之一,就值得考虑延迟依赖:
- 初始化昂贵且系统启动阶段不会立刻使用(比如大模型客户端、重型连接池、资源解析器);
- 修饰多重、容易产生循环依赖的对象;
- 希望在运行时动态决定到底用哪个具体实例的策略型依赖。
不过延迟依赖也不是越多越好。过度使用会牺牲些许性能(每次getObject()都要走容器查找流程)以及类型静态检查能力。在不需要提前解耦的场景,直接字段注入或者构造器注入反而更简单直观。
6. 深入Spring6实例:完整实现一个工厂模式改造案例
6.1 案例背景
考虑到前面讲了大量概念,现在用一个可运行的例子把内容串起来。假设我们要做一个消息推送系统,支持短信、邮件、站内信三种渠道。推送消息类型不同,对应的处理器逻辑不同。最直观但很蠢的写法是:
public void push(Message msg) { if ("sms".equals(msg.getChannel())) { // 发送短信逻辑 } else if ("email".equals(msg.getChannel())) { // 发送邮件逻辑 } else if ("inbox".equals(msg.getChannel())) { // 发送站内信逻辑 } }如果后续渠道多起来——比如增加微信、企微、钉钉——这段代码会越来越庞大,同时也严重违背了单一职责原则。我们的目标是用Spring6的注解驱动方式,把这个过程改造成一个基于工厂模式的优雅结构。
6.2 项目结构设计
先定义统一的策略接口:
public interface MessagePushService { void push(String title, String content, String target); boolean supports(String channel); }然后定义短信实现:
@Component public class SmsMessagePushService implements MessagePushService { @Override public void push(String title, String content, String target) { // 实际对接短信服务商API System.out.println("发送短信 -> 标题:" + title + ", 目标:" + target); } @Override public boolean supports(String channel) { return "sms".equals(channel); } }邮件和站内信的实现逻辑类似,只是支持的channel不同,一个返回"email",一个返回"inbox"。注意,这里每个渠道服务都注册成Spring的Bean,由容器统一管理生命周期。
6.3 工厂类实现
接下来是关键,定义一个渠道选择工厂,它就是整个模式的调度中枢:
@Component public class MessagePushFactory { private final List<MessagePushService> pushServiceList; public MessagePushFactory(List<MessagePushService> pushServiceList) { this.pushServiceList = pushServiceList; } public MessagePushService getPushService(String channel) { return pushServiceList.stream() .filter(service -> service.supports(channel)) .findFirst() .orElseThrow(() -> new IllegalArgumentException("No push service for channel: " + channel)); } }这段代码用了Spring最常用的一招:通过构造器注入一个List<MessagePushService>,Spring会自动把IoC容器中所有MessagePushService类型的Bean全部装配进来。未来新增一个渠道,只需要新写一个实现类并标注@Component,工厂本身一行都不用改,完全符合“开闭原则”。这种技巧在项目里解决“多实现路由”的问题非常常见,本质上就是Spring充当了策略模式的注册中心,而工厂则是策略的“调度员”。
6.4 使用方视角
在Controller或者其他业务层,用起来就很简单了:
@RestController public class PushController { private final MessagePushFactory pushFactory; public PushController(MessagePushFactory pushFactory) { this.pushFactory = pushFactory; } @PostMapping("/push") public void push(@RequestBody PushRequest request) { MessagePushService service = pushFactory.getPushService(request.getChannel()); service.push(request.getTitle(), request.getContent(), request.getTarget()); } }对比之前的if-else写法,业务侧彻底摆脱了渠道的分支判断。要新增渠道,改动的只是新增一个实现类,原有代码无侵入,测试起来也方便——可以针对每个渠道单独构造测试用例。
6.5 如何在此基础上扩展抽象工厂
如果渠道不止是“推送”功能,每个渠道还有自己的“回执查询”、“取消推送”等操作,那上面这个简单工厂策略组合就不够用了。此时可以做一次升级:每个渠道的处理器不再只是一个单独的服务,而是一整套组件,比如SmsProcessor包含SmsPusher、SmsReceiptQuerier、SmsCanceller。你可以定义一个抽象工厂:
public interface ChannelComponentsFactory { MessagePushService createPusher(); ReceiptQueryService createReceiptQuerier(); CancelPushService createCanceller(); }每个渠道对应一个ChannelComponentsFactory的实现,负责生成该渠道专属的一整套组件。这个升级路径很平滑,本质上是将原本的“选择单一服务”提升为“选择一群配套服务”。在大型项目中,这种从简单工厂到抽象工厂的演进几乎是必然的,因为业务维度会不断扩张,产品之间的“皮带”关系越来越复杂。
7. 常见问题与排查技巧实录
7.1 为什么getBean拿到的对象类型和我预期的不一样
出现这种情况,先别急着怀疑Spring坏了。检查一下你定义的类是不是实现了FactoryBean接口。如果是,那么直接getBean("xxx")拿到的是getObject()的产物,想要工厂本身就必须用&xxx。另外还要排查是否在某个@Bean方法里返回了代理对象或装配了切面,代理对象的类型往往和原始类型不同,按类型获取时需要调整搜索策略。
7.2 自动匹配多个Bean时抛NoUniqueBeanDefinitionException
这种情况在按类型获取Bean时很常见。比如定义了多个MessagePushService实现类,然后直接context.getBean(MessagePushService.class),Spring就会发愁不知道给你哪一个。解决办法有三种:
- 用工厂模式封装一个调度逻辑(就像上面案例做的那样);
- 注入
List<MessagePushService>或Map<String,MessagePushService>,自己决定用哪个; - 为某一个实现类标记
@Primary,让Spring默认选择它。
从架构整洁度上说,第一种方式最优,它把选择逻辑收敛到一处,业务代码完全不感知多实现的存在。
7.3 FactoryBean的getObject方法被重复执行
如果发现某些昂贵的创建逻辑被执行了很多次,大概率是getObject()配套的isSingleton()返回了false。默认情况下FactoryBean生产的对象不一定是单例——Spring官方文档里对isSingleton()方法语义有过说明,它影响的是getObject()返回对象的缓存行为。如果你的产品对象本身希望全局唯一,务必把isSingleton()设为true。另外还需要确认这个FactoryBean是否被多次注册或存在多个容器实例(比如父子容器),每个容器都会创建各自的一份工厂。
7.4 依赖注入的Bean为null但getBean能拿得到
这类问题通常涉及循环依赖或初始化顺序。当一个Bean的构造函数里注入了另一个尚未完成初始化的Bean,Spring可能会被迫通过三级缓存提前暴露工厂或代理。但某些边界条件下,它会选择返回一个未完全初始化的对象。排查方向包括:检查是否有两个Bean通过构造器相互引用(构造器循环依赖是Spring无法自动解决的)、检查是否在@PostConstruct或InitializingBean中提前调用了依赖Bean的方法、检查是否存在代理对象导致生命周期回调顺序被打乱。
7.5 关于工厂模式使用边界的个人心得
我做这个改造时的体会是:工厂模式在Spring项目里,绝大多数情况下不需要手动new对象,而是要善用容器自动装配机制。Spring的IoC本质上已经充当了工厂,所以我们要做的更多是“策略分发”而不是“手动创建”。真正需要自己写工厂类的地方,通常是创建过程特别复杂、创建逻辑需要复用、或者需要做一批产品的配套组装。把这个边界理清楚,能避免很多无意义的抽象。
另外要特别说明:工厂模式和策略模式经常搭在一起用。工厂负责“如何拿到正确的处理器”,策略负责“处理器内部如何展开差异逻辑”。如果只做策略模式而不做工厂分发,调用方就还得自己写一堆选择逻辑。反过来,如果只做工厂而不把策略抽出来,工厂内部又会膨胀成一个大杂烩。两者珠联璧合,才是真正优雅的工程结构。
8. 写在最后的几个实用建议
这篇笔记讲了这么多,最后再补几个我自己在实际项目里反复验证过的小心得,希望能帮大家少走弯路。
第一点,别急着上抽象工厂。很多团队一讨论设计模式,就总想一步到位用最高级形态。但一旦业务只有两个渠道,抽象工厂的收益根本覆盖不了类爆炸带来的维护成本。从简单工厂做起,等到业务真正出现多渠道多组件绑定的时候再平滑重构,这个过程是合理的。设计模式的应用一定是“被需求驱动”,而不是“被风格驱动”。
第二点,把工厂类的命名规范化。项目里建议统一用XxxFactory、XxxProvider、XxxResolver这类后缀,这样团队里其他人一眼就能看清这个类扮演的角色。不要出现叫XxxService里面却放着一大堆工厂逻辑的类,那会让维护者非常痛苦。命名清晰本身就是一种可读性资产。
第三点,结合Spring特性,能用容器机制解决的就别硬编码设计模式。比如多实现的自动装配(注入List<T>)、@Qualifier按名字选择、@Primary默认优先,这些框架自带的武器已经消化掉一半设计模式的需求,自己写的代码越少,出Bug的接口面就越小。工厂模式的核心价值是封装变化,Spring帮我们把创建过程都封装完了,我们要封装的是“选择逻辑”和“策略组合”,而不是重新发明一遍创建逻辑。
如果把整个Spring容器理解成一个超级工厂,我们所学的一切——BeanFactory、ApplicationContext、FactoryBean、ObjectFactory——都是这个超级工厂体系在不同层次、不同场景下的具体表现。想通这一点,工厂模式这篇算是真正过关了。