夜雨聆风学习资料网

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 源码:一张表看完

模式
书上例子
Spring实现
学到的核心
工厂
披萨店
BeanFactory / FactoryBean
收回创建权,才能统一管理
代理
房屋中介
AOP(JDK/CGLIB)
不动原代码,加一层皮
模板方法
泡咖啡泡茶
JdbcTemplate
不变的包办,变化的回调
观察者
气象站公告板
ApplicationEvent
容器当中介,互不认识
策略
促销活动
Resource体系
选择逻辑也独立成组件

— 最后 —不是设计模式书没用。是书的写法天然有局限——它得用简单例子把模式本身讲清楚,而简单例子撑不起"什么时候该用"这个真正难的问题。"什么时候该用"只能从真实代码里长出来。23种模式不用全记住。真正高频用的,一手数得过来。每一个把"为什么用它"想透了,写代码的时候它们会自己跳出来——发现一段逻辑在到处复制,代理跳出来;发现扇出需求打了一串Service,观察者跳出来;发现加一种类型要改老代码,策略跳出来。读源码也别贪多。Spring读不下去就从这几个点切进去:想搞懂代理,去看AopProxy;想搞懂工厂,去看FactoryBean;想搞懂模板方法,去看JdbcTemplate.execute()。带着问题读一个点,比从头到尾读一遍收获大得多。

读书是为了知道名字。读源码是为了长出手感。手感这东西,书上没有。

IN THE END模式不是拿来套的,是代码疼的时候自己长出来的。

下一次读源码,别只盯着"这个功能怎么实现的"。多问一句——"这里为什么非得用这个模式?不用会怎样?"答案比你背过的所有UML图都值钱。

你在源码里见过哪些"漂亮的设计"?评论区聊聊 👇

相关学习资料