ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot 九种核心设计模式实战:从源码到业务落地的完整指南

Spring Boot 九种核心设计模式实战:从源码到业务落地的完整指南 1. 为什么偏偏是这九种先说清楚选型逻辑如果搜索Spring Boot 设计模式满屏都是23种GoF设计模式的全量盘点。但说实话日常用Spring Boot做业务开发真正高频出场、直接影响代码结构的翻来覆去就那么几种。写这篇全览之前我特意回看了近几年经手的商城、OA、推荐系统、支付网关这类项目把真正在Spring Boot生态里被框架原生支持或者实践验证过的模式筛了出来最后圈定这九种单例、工厂、建造者、策略、模板方法、适配器、观察者、装饰器、责任链。为什么砍掉剩下那些比如解释器模式你在Spring Boot里一年能写几次还有享元模式除了连接池、常量池业务代码里几乎碰不到。不是它们没用而是对绝大多数做业务开发的人来说把上述九种吃透从Controller层到Service层再到基础设施层代码质量会有质的提升。而且这九种模式在Spring Boot内部就有大量现成的官方示范学的时候直接对照源码比自己瞎想直观得多。这篇全览的思路不是背书而是换一个角度每种模式先讲清楚它解决什么痛点然后看Spring Boot在哪些地方已经帮你实现了最后给出你自己动手落地的最短路径和踩坑点。全程用真实项目里的片段说话保证每一个示例你都能直接抄走改改就能用。2. 单例模式Spring容器默认给你的福利2.1 你其实一直在用只是没意识到Spring Boot的Bean默认就是单例的。很多新手写代码时根本没想过这个问题直到某天在Service里塞了一个带状态字段的对象猛然发现多个请求之间数据串了。这就是典型的单例误用。Spring管理的Bean默认scope就是singleton。这带来的核心好处有两个一是对象只创建一次省掉了频繁new的开销对无状态的Service、Mapper这类组件尤其友好二是依赖注入关系只需要建立一次整个容器启动时就把对象图拼好运行期直接复用。2.2 源码里的单例实现长什么样Spring容器本身对单例的管理是在DefaultSingletonBeanRegistry里完成的。它的核心逻辑是维护一个ConcurrentHashMapString, Object作为单例池调用getSingleton(String beanName)时先从池子里取取不到就走创建流程创建完再put回去。这里用了双重检查锁的思路来保证并发安全避免同一个beanName被重复创建。// 简化后的核心逻辑源码里比这更复杂 protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject this.singletonObjects.get(beanName); if (singletonObject null isSingletonCurrentlyInCreation(beanName)) { singletonObject this.earlySingletonObjects.get(beanName); if (singletonObject null allowEarlyReference) { // 从三级缓存取工厂解决循环依赖 } } return singletonObject; }这里顺带解释一个经常被面试问到的东西为什么Spring能解决构造器注入之外的循环依赖靠的就是三级缓存。一级缓存存成品二级缓存存半成品还没完成属性填充三级缓存存的是ObjectFactory用来提前生成代理对象。凡是用Autowired以字段或者setter方式注入的循环依赖Spring靠这个机制能兜住但构造器注入不行因为构造阶段就需要对方的实例此时对方还是半成品就报BeanCurrentlyInCreationException。这是一个很实用的认知点如果项目里出现构造器循环依赖报错你去调整字段注入能过但本质上是在掩盖设计问题最好重构拆分。2.3 实操注意事项自己写单例的时候我一般推荐交给Spring容器去管而不是自己手写一个getInstance()。手写单例意味着你要自己处理并发、序列化、类加载器等问题纯属给自己找不痛快。真正需要注意的反而是这几个容易踩的坑。单例Bean里绝对不要放会变化的成员变量用方法参数或者ThreadLocal来替代。Scope(prototype)并不意味着Spring每次都new一个给你前提是你通过ApplicationContext.getBean()获取。如果直接Autowired注入一个prototype的Bean到单例Bean里注入的仍然是同一个实例这个坑我在项目里真实遇过。异步场景下Async注解的方法是通过代理实现的如果你在同一个类内部调用this.asyncMethod()代理不生效单例加异步的叠加玩法就会失效。3. 工厂模式Spring容器它本身就是个大工厂3.1 从BeanFactory说起工厂模式的核心价值是让客户端不直接new对象而是通过一个工厂来获取从而把对象的创建和使用解耦。Spring Boot对整个开发过程的统治本质上就是工厂模式最极致的体现。你写Component、Service、Repository不做new操作对象就自动出现在需要的地方。ApplicationContext继承自ListableBeanFactory而BeanFactory最核心的两个方法是getBean(String name)和getBean(ClassT requiredType)。这也是为什么IoC控制反转和工厂模式看起来很像的原因——IoC是一种设计思想工厂模式是这种思想的具体实现手段之一。// 传统方式客户端直接依赖具体类型 OrderService orderService new LocalOrderServiceImpl(); // 工厂方式客户端只面向接口具体创建逻辑交给工厂 OrderService orderService (OrderService) applicationContext.getBean(orderService);3.2 更实用的静态工厂和Bean方法自己业务代码里最实用的工厂模式写法有两种一种是用静态工厂方法配合Map做策略分发。ServiceImpl里维护一个MapString, IPayHandlerkey是渠道码value是对应handler。外部调用方只需要传一个payType进去就能拿到正确handler完全不用写一堆if-else。另一种是Configuration类里的Bean方法。Spring通过Bean方法注册Bean时你可以自己决定返回类型是接口还是实现类可以加额外的初始化逻辑还可以通过方法参数触发其它Bean的注入。这种方式比直接在类上加Component更灵活特别是当Bean的创建需要依赖于配置项或者需要做复杂初始化时非常适合。Configuration public class CacheConfig { Value(${cache.type:redis}) private String cacheType; Bean public CacheService cacheService() { if (redis.equals(cacheType)) { return new RedisCacheService(); } return new LocalCacheService(); } }3.3 工厂模式和策略模式怎么配合工厂模式和策略模式在实际项目中经常成对出现策略模式定义了同一件事的多种做法工厂模式负责根据条件选出一个做法。比较经典的组合是策略接口 策略实现类每个策略一个Service 一个Factory/Selector在Factory里注入ListStrategy并转成MapString, Strategy。这里有一个Spring注入的小技巧非常实用当你向容器注册多个同类型Bean时可以直接注入ListT拿到所有Bean或者注入MapString, T拿到带名字的Bean列表。这个技巧是工厂模式在Spring Boot里最优雅的落地方式具体示例我在策略模式那一节会展开讲。4. 建造者模式链式API背后的优雅设计4.1 让代码说话的例子先看一段代码User user User.builder() .name(张三) .age(25) .email(zhangsanexample.com) .address(北京市海淀区) .build();写起来顺滑读起来清楚比写一个五个参数的构造函数或者连续调用五个setter好看多了。这就是建造者模式在Java世界里最常见的表现形态Builder链式调用。它的本质是用一个独立的Builder对象逐步设置参数最后用一个build()方法一次性产出完整的目标对象。Spring Boot生态里到处是这个模式。比如UriComponentsBuilder用来拼URL、SpringApplicationBuilder用来配置应用启动参数、还有Spring Data的ExampleMatcher、Elasticsearch的BoolQueryBuilder都是链式API的典型。4.2 为什么构造函数和JavaBean都差点意思构造函数方式的问题在于参数多了以后可读性极差四个同类型参数排一排传错顺序的bug非常隐蔽。JavaBean方式无参构造 setter好一些但又带来两个新问题一是对象在设置过程中处于中间态可能被并发线程读到不完整的数据二是Setter的存在让对象失去了不可变性后续被谁改了你都不知道。建造者模式解决得干净利落构建过程中Builder持有的是待提交的数据目标对象在build()时才被不可变地创建出来参数可以按任意顺序设置不用记顺序可以在build()方法里做统一校验比如某个字段必须非空、两个字段必须互斥。4.3 手写一个Builder和Lombok的坑自己写Builder的代码不多因为Lombok的Builder注解一行搞定。但用Lombok有两个坑必须提醒。第一个坑是继承。父类字段用Builder子类继承后子类的Builder默认只能设置子类自己的字段父类的字段不见了。解决方案是在父类和子类上都加Builder然后子类上再加SuperBuilder。SuperBuilder专门为继承场景设计。第二个坑是构造器冲突。Builder默认生成全参数构造器如果你同时用了NoArgsConstructor两者会冲突编译失败通常需要显式加上AllArgsConstructor或者Builder不生成构造器配合Tolerate处理。报错信息会明确告诉你constructor with no arguments is not defined遇到时别慌加个NoArgsConstructor在类上或者用Builder(builderMethodName internalBuilder)改名即可。还有一点Builder适合参数多、且参数之间有校验关系的场景。如果就一两个字段直接构造函数就好不要为了用模式而用模式。5. 策略模式消灭几百行if-else的大杀器5.1 什么时候该用策略模式后端开发里最俗的业务就是根据类型走不同逻辑。支付要区分支付宝、微信、银联通知要区分短信、邮件、App推送订单状态要区分待付款、已付款、已发货、已完成。新手直接写if-else分支少的还好一旦分支超过四五个方法就变成一坨看不清逻辑的面条代码。而且每加一种新类型就要改动核心方法违背了开闭原则。策略模式的标准结构是三层策略接口定义统一的动作方法策略实现类每一种类型一个实现负责自己的具体逻辑上下文/选择器根据传入的参数决定选哪个实现。5.2 Spring Boot里最优雅的实现方式Spring Boot做策略模式几乎是为这个场景量身定做的。核心思路是用一个Map来承接Spring注入的所有策略Bean。// 1. 策略接口 public interface PayHandler { String getPayType(); void pay(BigDecimal amount); } // 2. 策略实现 Service public class AliPayHandler implements PayHandler { Override public String getPayType() { return alipay; } Override public void pay(BigDecimal amount) { // 调用支付宝SDK System.out.println(支付宝支付 amount); } } // 3. 选择器 Service public class PayHandlerSelector { private final MapString, PayHandler handlerMap; public PayHandlerSelector(ListPayHandler handlers) { this.handlerMap handlers.stream() .collect(Collectors.toMap(PayHandler::getPayType, Function.identity())); } public PayHandler getHandler(String payType) { PayHandler handler handlerMap.get(payType); if (handler null) { throw new IllegalArgumentException(unsupported pay type: payType); } return handler; } }Spring会把所有PayHandler的实现类按ListPayHandler注入构建函数里把它们转成Map。选择器对外只暴露一个getHandler(String)方法以后新增一种支付方式只需要写一个新的实现类选择器一行代码都不用改。这是开闭原则落地得很漂亮的一种写法。5.3 两个复杂场景的进阶思路场景一策略本身有优先级。比如多渠道通知场景短信想作为默认渠道邮件作为高级渠道App推送作为补充渠道。可以在策略接口里增加一个getOrder()方法注入List之后先按order排序然后根据某种规则依次选择。这种写法把策略选择策略的元逻辑也固定下来后续调整优先级只需要改order值。场景二多条件组合匹配。如果选择策略不只看一个类型字段而是根据一组条件来判断比如地区用户等级支付方式可以把getHandler的参数设计成一个PayContext对象策略接口统一改成boolean support(PayContext context)选择器遍历handler列表找第一个support()返回true的。这其实是策略模式和责任链模式的一种融合变体非常实用。场景三让策略自注册而不依赖Spring启动顺序。如果项目对启动速度敏感或者Bean初始化顺序刁钻可以用ApplicationRunner/SmartInitializingSingleton在所有Bean创建完成后再做一次Map组装避免在构造器阶段就访问容器。6. 模板方法模式把流程骨架和可变步骤分开6.1 生活中的例子和框架里的模板方法煮咖啡和泡茶同样的流程烧水 - 冲泡 - 加料。不同的只是冲泡的东西和加的料不同。把不变的部分写成父类方法把变化的部分留给子类实现这就是模板方法模式。Spring Boot里到处是模板方法。最典型的是JdbcTemplate。它把获取连接、创建Statement、执行SQL、处理结果集、释放连接这一整套JDBC流程固定好把变化的部分SQL语句、参数、结果映射留给你传入的回调接口。你不需要关心数据库连接怎么管理只管写SQL和读结果。再看RestTemplate和JdbcTemplate的设计你会发现它们都是模板方法 回调的组合模板方法定义了骨架回调接口让你填变化部分。这种组合比单纯的继承式模板方法更灵活因为它用组合替代了继承Spring Boot的很多模板类都采用了这个模式。6.2 自己写一个业务模板方法一个典型的业务例子多种类型报表导入导出。不管是订单报表、用户报表还是商品报表流程都是一样的——校验文件格式、读取数据、逐条校验、批量落库、返回结果。这些步骤里读取数据和逐条校验的细节各不相同批量落库则往往是同一个方法。public abstract class AbstractImportTemplate { // 模板方法定义骨架注意用final修饰防止子类重写 public final ImportResult execute(MultipartFile file) { checkFile(file); ListMapString, Object rawData readData(file); ListString errors validateData(rawData); if (!errors.isEmpty()) { return ImportResult.fail(errors); } int count saveData(rawData); return ImportResult.success(count); } protected abstract ListMapString, Object readData(MultipartFile file); protected abstract ListString validateData(ListMapString, Object rawData); protected abstract int saveData(ListMapString, Object rawData); private void checkFile(MultipartFile file) { if (file.isEmpty()) { throw new BizException(文件不能为空); } // 检查文件后缀、大小等 } }子类只需要实现三个抽象方法骨架自动运行。后面如果需要在导入前加一步数据清洗只需要改父类的模板方法所有子类同步生效。6.3 模板方法的坑和习惯比较常见的一个错误是子类重写了execute()这个模板方法本身导致骨架失控。父类把模板方法用final修饰是一个好习惯如果不想用final至少要清晰命名比如executeTemplate并写明注释禁止覆盖。另一个经验是模板方法层数不要嵌套太深。我见过三层的模板继承改一个底层方法一堆子类行为全变了排查问题要从三个类里来回跳非常痛苦。如果想增加复用但又不想加深继承层次优先考虑组合方式把公共流程抽成一个Processor类的公共方法把变化部分通过函数式接口传入。public class ImportProcessor { public ImportResult execute( MultipartFile file, FunctionMultipartFile, ListMapString, Object reader, FunctionListMapString, Object, ListString validator, FunctionListMapString, Object, Integer saver) { // 骨架逻辑同上 } }这种写法更加灵活复用性也不差对新手更友好推荐优先考虑。7. 适配器模式让本来不匹配的接口能配合工作7.1 适配器解决的问题本质现实中的充电器你有一个Type-C的手机但手头只有Micro-USB的线需要一个转接头才能充进去。适配器模式做的事情跟这个一模一样把一组接口转换成客户端期望的另一组接口。在Java里就是写一个Adapter类把目标接口的方法调用翻译成被适配者的对应方法。Spring Boot里适配器的经典场景是HandlerAdapter和HandlerMethodArgumentResolver。这里挑HandlerMethodArgumentResolver来说因为它更贴近业务开发。7.2 自定义一个参数解析器Spring MVC在处理Controller方法入参时会调用HandlerMethodArgumentResolver来把HTTP请求里的数据转换成方法的实参。默认情况下RequestParam能拿到query参数RequestBody能拿到JSON体。但是如果前端传的是经过加密的报文你希望解密后自动放进去就需要自己写一个ArgumentResolver。Component public class DecryptArgumentResolver implements HandlerMethodArgumentResolver { Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(DecryptParam.class); } Override public Object resolveArgument(...) throws Exception { // 1. 从request里取原始密文 // 2. 解密得到明文JSON // 3. 用ObjectMapper把JSON转成目标类型 return targetObject; } }然后在配置类里把这个Resolver注册到WebMvcConfigurerConfiguration public class WebConfig implements WebMvcConfigurer { Autowired private DecryptArgumentResolver decryptArgumentResolver; Override public void addArgumentResolvers(ListHandlerMethodArgumentResolver resolvers) { resolvers.add(decryptArgumentResolver); } }这样一来Controller层的方法签名清爽很多跟解密相关的逻辑全部收敛到适配层每个方法只需要加一个DecryptParam注解。7.3 适配器的四个变体适配器模式不仅能做类适配继承被适配者、对象适配持有被适配者引用在Spring Boot开发里更多是以对第三方SDK的封装形式存在。比如接微信支付时微信SDK的签名算法和参数格式跟你的内部接口差异很大写一个WechatPayClientAdapter包住SDK对外提供你自己定义的方法。这样一来以后换支付渠道Controller和Service都不用动只需要换个适配器。这是一个很值得养成的习惯所有和外部系统交互的地方一定要加一层适配器/防腐层不要让第三方的数据类型污染你的核心领域模型。适配器模式和装饰器模式容易混淆。适配器的目的是接口转换让原本不能一起用的东西一起用装饰器的目的是行为增强在原有接口之上增加新行为。别搞混面试和实际设计都会有用。8. 观察者模式事件的发布与订阅解耦8.1 业务场景和直接耦合的痛点最常见的场景就是下单成功以后要干什么发短信、发邮件、送积分、更新推荐位、通知仓库备货。如果直接在下单方法里一行行写业务逻辑会越来越长更麻烦的是下单成功这个事件一旦有新的消费者加入你必须改动下单方法本身。观察者模式的做法是下单方法只负责发布一个OrderCreatedEvent谁关心这个事件谁就去监听。发布方不需要知道有哪些监听者监听者之间互不干扰。这就是发布-订阅关系的核心价值发送方和接收方彻底解耦。8.2 Spring Boot里的EventListener实战Spring的事件机制就是观察者模式的官方实现三个核心组件ApplicationEventPublisher发布者、ApplicationEvent事件对象、EventListener监听处理器。// 1. 定义事件对象可以不用继承ApplicationEvent普通POJO即可 public class OrderCreatedEvent { private Long orderId; private Long userId; // getter/setter/constructor省略 } // 2. 发布事件 Service public class OrderService { Autowired private ApplicationEventPublisher eventPublisher; Transactional public void createOrder(OrderCreateDTO dto) { // 1. 业务逻辑 // 2. 保存订单 // 3. 发布事件 eventPublisher.publishEvent(new OrderCreatedEvent(orderId, userId)); } } // 3. 监听处理 Component public class OrderEventListeners { EventListener Async // 可以异步处理不影响主流程 public void handleSmsNotify(OrderCreatedEvent event) { // 发短信 } EventListener public void handleIntegral(OrderCreatedEvent event) { // 送积分 } }8.3 事件机制的三个关键秘诀第一个秘诀是事务绑定。TransactionalEventListener可以把监听器绑定到事务的某个阶段执行。比如phase TransactionPhase.AFTER_COMMIT表示事务提交成功后才触发监听器如果事务回滚了监听器不会执行。这个在高并发的订单场景非常实用避免事务还没提交就发了消息结果下单失败但短信已发出的严重bug。第二个秘诀是异常隔离。监听器里如果抛了异常默认会影响发布事件的线程导致主流程失败。建议每个监听器都用try-catch包住或者配合Async异步处理再或者实现ErrorHandler单独处理异常。不要因为一个监听器的失误把整个下单流程拖垮。第三个秘诀是顺序控制。多个监听器同时监听同一个事件时执行顺序默认是不确定的。如果确实有先后依赖可以用Order(1)、Order(2)来显式指定顺序。在事件设计时尽量让每个事件只关注一种职责不要创建一个上帝事件然后让所有逻辑监听它那样事件机制反而变成了隐式的调用地狱。9. 装饰器模式为对象动态加功能9.1 为什么不用继承去加功能假设有一个ReportService接口现在要求给所有报表的生成过程加一个敏感数据脱敏的能力。如果直接用继承你得为每个报表实现类都建一个子类并覆盖生成方法类数量翻倍。更麻烦的是如果还想叠加操作日志记录和结果缓存继承的组合会爆炸。装饰器模式的核心思路是用一个包装类持有原对象同时实现原对象的接口在调用原方法的前后做额外的处理。因为包装类也实现了同一个接口所以它可以层层嵌套组合出各种不同的能力。9.2 Java IO里的最典型示例Java的IO库是装饰器模式最出名的地方。new BufferedInputStream(new FileInputStream(test.txt))就是一层一层地包装FileInputStream是基础组件BufferedInputStream给文件流添加缓冲功能FilterInputStream家族全是装饰器。Spring Boot里也有几个重要的影子。比如ContentCachingRequestWrapper和ContentCachingResponseWrapper在过滤器里包装原始的HttpServletRequest/HttpServletResponse从而让请求体可以被反复读取原始流只能读一次Controller读完Filter就没了。WebFilter(/*) public class PrintRequestLogFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest (HttpServletRequest) request; ContentCachingRequestWrapper wrapper new ContentCachingRequestWrapper(httpRequest); chain.doFilter(wrapper, response); // 从wrapper.getContentAsByteArray()里取出请求体打印日志 } }这个是实际项目里很典型的需求打印请求日志。不用这个Wrapper的话Controller一旦读取了body过滤器里就读不到了使用包装类相当于把body缓存了一份。9.3 自己写装饰器还是用AOP理论上装饰器模式可以用AOP来替代Spring Boot里Aspect切面也能实现方法前后加逻辑的效果。实际开发中怎么选择我的经验是如果你只控制Spring容器里的Bean并且处理逻辑是横向的日志、权限、事务这类用AOP更省事如果你需要包装的对象不一定是Spring管理的Bean或者需要嵌套组合不同的行为装饰器模式更直接。另外有一种场景装饰器特别合适——在业务代码里扩展接口。比如CacheWrapper包装了一个RemoteUserService先查本地缓存缓存miss再请求远程。这种包装不需要网络服务本身有任何改动只需要你手动new CacheWrapper(remoteService)即可。装饰器模式的坑主要在透明性。客户端调用时可能感知不到代理的存在如果包装类没有正确转发方法或者对某些方法的处理不一致会产生诡异bug。建议装饰器只做前置 后置的增强逻辑不要改变原方法的功能语义否则就变成混入了策略模式逻辑会乱。10. 责任链模式让每个处理器各管一段10.1 Spring和Servlet里的责任链责任链模式在Spring Boot生态里最出名的两个例子是Servlet的FilterChain和Spring MVC的HandlerInterceptor。FilterChain说白了就是一堆Filter串成一条链每个Filter调用chain.doFilter()把请求传给下一个Filter。每个Filter只做自己负责的那部分编码处理、登录校验、跨域配置、日志打印一个Filter负责一件事请求像流水线一样依次穿过。拦截器HandlerInterceptor的preHandle、postHandle、afterCompletion三个方法也是责任链的变形。如果你配置了两个InterceptorSpring MVC按注册顺序依次执行preHandle再逆序执行postHandle和afterCompletion——这个执行顺序弄反了或者没理解排查拦截器问题时很容易懵。10.2 业务代码里的责任链写法当你遇到同一份数据需要经过多道合规检查或者一个请求需要多个handler依次尝试处理的场景责任链模型很合适。比如一个风控系统一笔支付请求要依次经过黑名单校验、频次校验、金额阈值校验、设备指纹校验。如果全部写在Service里方法会非常长且难以扩展。用责任链的方式public interface RiskHandler { void handle(RiskContext context); void setNext(RiskHandler next); } Component public class BlackListHandler extends AbstractRiskHandler { Override public void doHandle(RiskContext context) { if (blackListService.contains(context.getUserId())) { context.setRejected(true); context.setRejectReason(BLACK_LIST); return; } // 通过则交给下一个处理器 } }这里要注意一点责任链模式有两种流传方式。一种是每环都处理然后传给下一环像Filter另一种是遇到能处理的就直接终止像Netty的pipeline可以短路。设计时要先定义清楚语义避免后续维护混乱。10.3 责任链导致的性能与可观测性问题责任链模式最被吐槽的点是链路长、出问题时不好定位。一次请求走七八个handler报错了你都不知道坏在第几个。我的实践习惯是给每个handler加一个处理器名称和能力标识在入口和出口都打日志。入参出参都打印每个handler的执行耗时单独统计这样问题出现时能通过日志快速定位是哪一环出了问题。还有就是要给链路设置最大长度或者超时时间防止某个handler死等导致整个请求卡死。另外Spring Boot 3.x里出现了一个新的流式API叫ReactiveSecurityContextHolder相关的SecurityFilterChain排行直接暴露了责任链在Spring Security中的样子。如果你用过Spring Security会发现整个认证/授权流程就是一条很长的责任链定制化开发就是在链上插入自己的Filter。理解了责任链读Spring Security的源码会轻松很多。11. 九种模式如何组合实战一个支付模块的设计复盘11.1 一个贴近真实业务的综合案例我曾经参与过一个多商户商城系统的支付模块设计那个模块几乎把上面的模式全用上了。现在回看正好可以作为综合应用案例来讲一下。整体结构是这样的入口Controller接收支付请求参数包含支付渠道(payType)、订单号、金额等用一个PayContext对象承载上下文Controller调用PayService.pay(PayContext)PayService内部通过一个PayHandlerSelector工厂 策略拿到具体的支付handler每种支付渠道支付宝、微信、银联、余额各自实现PayHandler接口策略PayHandler的execute方法里面再走一条责任链风控校验 - 限额校验 - 实际调用SDK - 记录流水支付成功以后PayEventPublisher发布PaySuccessEvent观察者监听器里分别处理订单状态更新事务监听、发短信通知异步、赠送积分异步参数构造一律用BuilderSDK调用的差异封装在Adapter里给handler增加日志、重试能力的包装类装饰器整条流程的骨架定义在一个抽象模板类里新增渠道只需继承模板并实现必要的抽象方法。这套设计的好处是新增一个支付渠道只需要写一个新的PayHandler实现类工厂里自动注册责任链和事件机制自动接管后面的事情完全符合开闭原则。团队里每个人负责一个渠道的开发互相之间没有任何代码冲突。11.2 模式的使用边界和成本组合使用设计模式时最怕的是炫技式设计——为了模式而模式。如果一个接口只有两个实现一个方法只有三行逻辑硬套一个策略模式加一个工厂模式只会让代码更难读。我的个人判断标准是三个分支数大于等于3才考虑策略模式后续扩展可能性大才考虑工厂模式做注册事件发生后有不止一个独立动作才考虑观察者模式。产品经理告诉你后面可能会加xx功能这种可能不要作为设计依据。设计依据应该是现在已经有xx扩展需求或者这块代码历史上有过多次改动每次都要动核心逻辑。模式是服务可维护性的工具不是用来展示技术的摆件。11.3 团队内推行设计模式的经验代码评审的时候我经常看到有人把设计模式写成天书。比如一个策略工厂里塞了泛型、塞了函数式接口、塞了SPI机制新人看得云里雾里。实际上好的设计模式代码应该具备两个特征一是命名反映意图类名直接告诉别人这个类在工厂里起什么作用二是链路清晰从一个入口开始能顺着代码完整地走完一条流程不跳跃、不循环依赖。建议在团队里先建立模式的最小约定哪些场景必须用策略、哪些场景必须用事件、命名规范是什么。把这些约定写进团队代码规范文档代码评审里按规范检查。大量实践证明与其让每个人自己悟设计模式不如定好规范让所有人按一个相对统一的方式落地这样代码可读性提升比设计模式本身的影响更大。12. 从源码到编码习惯设计模式的下一层思考看Spring Boot源码时可能会有一个感觉很多类并不是严格对应某一种设计模式而是多种模式混在一起。DispatcherServlet是前端控制器模式加模板方法模式BeanFactory是工厂模式加注册表模式AopProxyFactory是工厂模式加代理模式Observable相关的事件体系是观察者模式加中介者模式。真实世界的复杂设计很少是单一模式准确识别的意义在于让你理解作者的设计意图而不是去贴标签。我自己在实际开发中的体会是设计模式最大的价值在于提供了一套命名过的解决方案。当你和同事讨论问题时说这里用策略模式重构一下双方脑海里会立刻浮现出那套结构省去了大量解释成本。但如果你只是记住了每一种模式的UML图却不知道它们各自在Spring Boot里长什么样大概率会在错误的场景里用错误的方式强行套用。最后分享一个很实用的练习方法挑一个自己维护的旧项目找出两三个最臃肿的方法试着用策略模式和模板方法去重构。不要贪多一次重构一个方法比较重构前后的代码量、可读性和后续改动成本。反复几次之后你会慢慢建立起模式直觉写新代码时自动就会往合适的方向设计。设计模式不是背诵出来的是踩坑和重构踩出来的。真到了那个阶段你再回来看这份全览会发现每种模式背后都站着一堆自己写过的血泪代码而那时候它们才真正长在了你的脑子里。
返回列表