ARTICLE · 1177711
如何结合源码合理的使用设计模式

SOURCE CODE · 设计模式实战10本设计模式书没讲透的东西Spring源码用5个真实案例教会了我工厂 · 代理 · 模板方法 · 观察者 · 策略 —— 每个都讲"为什么"
设计模式书告诉你"它长什么样",源码告诉你"它为什么存在、什么时候该用"。
— 一个尴尬的事实 —工作第三年,我把《Head First设计模式》和《设计模式之禅》都啃完了。23种模式,UML图画得滚瓜烂熟。面试被问"讲讲策略模式",对答如流。但写业务代码的时候,我一行都没用上。不是不想用,是不知道什么时候该用。书上的例子全是鸭子、披萨、咖啡加料。看得懂,但跟我写的订单、库存、支付完全不搭边。后来因为排查一个事务失效的Bug,第一次认真翻Spring源码。三个月断断续续读下来,发现一件事——
Spring源码就是一本活的设计模式教科书。但它的教法跟书完全不一样。书上讲"是什么"。源码讲"为什么"和"什么时候"。知道了为什么,模式才会自己跳出来——而不是你绞尽脑汁去套它。

— 01 —
工厂模式:不是"创建对象",是收回创建权
书上的工厂模式:一个PizzaStore抽象类,createPizza()是抽象方法,不同的子类工厂生产不同的披萨。讲的目的是"把对象的创建延迟到子类"。看完我会了。然后我在业务代码里写了一个OrderFactory。写完发现——除了多了一层转发,什么都没变。new改成工厂.new,纯属脱裤子放屁。
Spring源码里的工厂是另一回事。BeanFactory是顶层接口,就定义了一个契约:getBean()——"给我一个对象"。底下DefaultListableBeanFactory是真正的工厂,所有Bean的创建都从它这儿过。更妙的是FactoryBean——getObject()方法让你自己定制创建逻辑。MyBatis的Mapper接口没有实现类,@MapperScan扫描之后,就是靠MapperFactoryBean.getObject()给你返回一个动态代理对象的。
FactoryBean的核心契约(简化)public interface FactoryBean<T> { T getObject() throws Exception; // 创建逻辑你来定 Class<?> getObjectType();}// Mapper接口没有实现类?// getObject()里用JDK动态代理造一个,你拿到的"实现"就是这么来的
源码读到这里,我才看懂工厂模式的本质——工厂模式不是"new一个对象",是把"创建权"从调用方手里收回来。收回来才能做统一管理:单例、代理、依赖注入、生命周期回调、初始化前后处理——Spring的整套能力,全都建立在"创建权在我手里"之上。你的工厂如果只是把new换个地方,那不叫工厂模式,那叫多此一举。创建权收回来之后,你打算在这个"创建点"上做点什么?想清楚这个问题,工厂才用对了。
— 02 —
代理模式:不动原代码,加一层皮
书上的代理模式:Subject接口,RealSubject是真实对象,Proxy包着它,访问前先过Proxy。讲的是"控制访问"。看完觉得像房屋中介。然后呢?业务代码里我用它干嘛?
Spring源码里的代理,是我见过的最漂亮的用法。@Transactional为什么加一个注解,事务就自动开了?因为你的UserService被换成了代理对象。真正执行的时候,代理先开事务,再调用你的真实方法,最后提交或回滚。那段"开启事务、提交、回滚"的代码,被包在TransactionInterceptor里,就在代理层。你的业务方法里一行事务代码都没有。
选JDK还是CGLIB?判断逻辑就在这里// DefaultAopProxyFactory.createAopProxy()(简化)if (config.isProxyTargetClass() || targetClass == interfaceClass || targetClass.isProxyClass()) { return new ObjenesisCglibAopProxy(config); // 无接口/强制类代理}return new JdkDynamicAopProxy(config); // 默认走JDK动态代理// 那个著名的"事务失效"坑:内部方法调用不经过代理,拦截器根本没机会执行
书上说的"控制访问"太抽象了。源码教的是具体的——代理是"不动原代码,加一层皮"。日志、监控、权限、缓存、重试、事务——全是这一层皮。什么时候该想到代理?当你发现一段横切逻辑(跟业务无关但每个方法都要)在到处复制粘贴的时候。顺带把那个面试必问的坑讲明白了:为什么this调用事务会失效?因为this是真实对象,不是代理对象,这层皮根本没被碰到。书上永远不会这么教你。

— 03 —
模板方法:不变的包办,变化的回调
书上的模板方法:Coffee和Tea继承自CaffeineBeverage,prepareRecipe()固定了"烧水、冲泡、倒杯、加料"的骨架,子类只覆写差异步骤。还配了个好听的词叫"钩子方法"。JdbcTemplate是同样的思想,但实现方式完全不同。
手写JDBC都经历过:获取连接、创建Statement、执行、遍历结果、关连接关Statement关ResultSet。50行代码里,真正跟业务有关的就一句SQL和结果映射,其他全是模板代码。JdbcTemplate把不变的骨架全包了。变的部分——SQL怎么写、结果怎么映射——抽象成两个参数让你传进来。
从继承版到回调版// 书上的版本:靠继承,子类覆写钩子方法class Tea extends CaffeineBeverage { void brew() { /* 用茶叶泡 */ }}// Spring的版本:靠回调,差异当参数传进去jdbcTemplate.query( "SELECT * FROM users WHERE age > ?", (rs, i) -> new User(rs.getLong("id"), rs.getString("name")), 18);// 连接、Statement、异常翻译、资源关闭——全在框架里,你永远不用碰
两种实现的核心区别:继承版的骨架一棵继承树只能有一个,想换骨架就得重构继承体系。回调版的骨架随便换,每调用一次就能带一个新差异进去,还能自由组合。这就是为什么lambda出来之后,Spring全家桶的模板类都改成了回调风格。继承版模板方法是"固定套路",回调版是"可插拔套路"。模板方法的本质:把"不变的"圈起来包办,把"变化的"挖个坑让调用方填。这个"挖坑"的思路,在你的代码里到处能用——批量任务框架把"读数据、并行执行、错误处理"圈起来,把"单条怎么处理"挖坑;导出功能把"取数、分页、写文件"圈起来,把"每行怎么渲染"挖坑。
— 04 —
观察者:解耦的完全体是互不认识
书上的观察者模式:气象站WeatherData,公告板CurrentConditionsDisplay。registerListener()注册、notifyObservers()通知。讲的是"一对多依赖"。书上的版本有个没说透的地方:观察者和被观察者还是要互相认识——至少被观察者得拿着一串观察者列表。
Spring的事件机制把这一步也解掉了。publisher只管发事件——applicationEventPublisher.publishEvent(new OrderCreatedEvent(order))。listener只管听——@EventListener注解一挂,Spring容器自动把两者接上。双方从头到尾不知道对方存在,都只认识Spring容器。容器当中介,才是解耦的完全体。
订单创建后的扇出场景// 订单服务:只发事件,不关心谁听orderService.create(order);publisher.publishEvent(new OrderCreatedEvent(order));// 积分服务:想加积分就来听@TransactionalEventListener(phase = AFTER_COMMIT)public void addPoints(OrderCreatedEvent e) { ... }// 明天产品说加个"下单发短信"——新写一个Listener,订单服务一行不用改
@TransactionalEventListener(phase = AFTER_COMMIT)这个细节值得单独说:事务没提交就发事件,监听方可能查到不存在的数据。AFTER_COMMIT保证提交后才触发,这是源码和文档里学来的、踩过坑才知道的细节。判断要不要用观察者的标准:当"下单后要发短信、加积分、推推荐"这种扇出需求出现时,如果你在订单代码里注入了三个Service——该解耦了。
— 05 —
策略模式:把"选择逻辑"也独立出去
书上的策略模式:促销活动,满减一个策略、打折一个策略、Context里持有Strategy引用。讲的是"算法可替换"。书上的例子有个隐藏问题——策略的选择逻辑还是写在客户端代码里:if (type == 满减) ... else if (type == 打折) ...。策略类是解耦了,选择逻辑又长成了一坨if-else。
Spring的Resource体系是教科书级示范。ClassPathResource、FileSystemResource、UrlResource、ServletContextResource——四个策略实现同一个接口。但你的代码里看不到任何一个if。DefaultResourceLoader根据前缀"classpath:"、"file:"、"http:"自动选。策略模式+工厂/注册器组合,客户端连策略类名都不用知道。
从源码学来落到业务的支付渠道路由public interface PayStrategy { String channel(); // 策略自报家门 PayResult pay(PayRequest req);}// 启动时把所有策略按channel()收进Map——选择逻辑死了@PostConstructvoid init() { strategies.forEach(s -> map.put(s.channel(), s));}// 调用时一行,没有if-elsemap.get(req.getChannel()).pay(req);
策略模式真正要消灭的不是if-else本身,是"加一种策略要改老代码"这件事。加一个银联支付,写一个新实现类,注册进Map——对已有代码零改动。这叫对扩展开放、对修改关闭。书上只告诉你原则,源码告诉你怎么落地成一行map.get()。
书上 vs 源码:一张表看完

— 最后 —不是设计模式书没用。是书的写法天然有局限——它得用简单例子把模式本身讲清楚,而简单例子撑不起"什么时候该用"这个真正难的问题。"什么时候该用"只能从真实代码里长出来。23种模式不用全记住。真正高频用的,一手数得过来。每一个把"为什么用它"想透了,写代码的时候它们会自己跳出来——发现一段逻辑在到处复制,代理跳出来;发现扇出需求打了一串Service,观察者跳出来;发现加一种类型要改老代码,策略跳出来。读源码也别贪多。Spring读不下去就从这几个点切进去:想搞懂代理,去看AopProxy;想搞懂工厂,去看FactoryBean;想搞懂模板方法,去看JdbcTemplate.execute()。带着问题读一个点,比从头到尾读一遍收获大得多。
读书是为了知道名字。读源码是为了长出手感。手感这东西,书上没有。
IN THE END模式不是拿来套的,是代码疼的时候自己长出来的。
下一次读源码,别只盯着"这个功能怎么实现的"。多问一句——"这里为什么非得用这个模式?不用会怎样?"答案比你背过的所有UML图都值钱。
你在源码里见过哪些"漂亮的设计"?评论区聊聊 👇















































































