ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Java行为型设计模式实战:从策略到状态模式重构业务代码

Java行为型设计模式实战:从策略到状态模式重构业务代码 接手一个维护了五年的支付系统时我最怕改的是一段三百行的下单方法。里面同时混着支付渠道判断、折扣计算、配送方式分流和消息通知每次需求变更都得在层层if-else里找插入点看完一半就忘了前一半的逻辑。重构之后再看核心流程只剩十几行旁边多了一组七八个类每个类只干一件事。这次重构用到的东西就是Java行为型设计模式。行为型设计模式是23种GoF设计模式中数量最多的一类总共11种分布在对象之间如何通信、职责如何分配、算法流程如何组织这一个核心命题下。很多Java开发者对设计模式的第一印象是面试八股文或者软考、期末考的背诵清单但如果你真正经历过老项目的维护你会发现这些模式本质上就是一套代码变动的预案——哪里容易变就用哪个模式把变化隔离出来。这篇文章面向想要搞懂行为型模式而不只是背名的Java学习者、正在准备面试和软考的开发者以及那些在项目里隐隐觉得if-else失控、想找一条重构出路的同行。1. 行为型模式全景这11个模式到底在管什么1.1 为什么行为型模式数量最多23种设计模式分三类创建型5种管对象怎么创建结构型7种管对象怎么组合行为型11种管对象怎么协作。数量上有明显差异这不奇怪——创建一个对象、拼装一个结构的套路远比对象之间互相打交道的方式要少。两个对象之间可以单向通知、可以双向通信、可以层层传递请求、可以委托第三方协调、还可以把请求本身封装成一个对象每一种打交道方式的差异都演化出一个模式。行为型模式解决的核心问题有三个对象之间的通信观察者、中介者、迭代器、职责的分配与传递策略、责任链、命令、模板方法、访问者、状态的维护与恢复状态、备忘录、解释器。理解了这三个维度你就不会觉得11个模式是一堆散点而是围绕对象协作展开的三条线索。1.2 按协作方式给11个模式分组分组模式核心思想典型场景算法与流程封装策略、模板方法把可变部分抽取出来固定部分留在原地算法替换、多步骤导入流程请求传递与职责分配责任链、命令、中介者请求从一个对象流到另一个对象审批流、撤销重做、多人协作状态与时间维护状态、备忘录状态变化流转历史状态快照恢复订单状态机、文本撤销对象通信与遍历观察者、迭代器、访问者通知、遍历、在结构上增加操作事件监听、集合遍历、报表导出语法语义解释解释器定义文法并解释执行表达式解析、自定义规则引擎还有一种经典分法模板方法和解释器属于类行为型模式靠继承实现其余9种属于对象行为型模式靠组合实现。做软考题的时候判断题模板方法模式是类行为型模式是对的这是个高频考点。1.3 记忆口诀与复习抓手网上流传的23种设计模式记忆口诀版本很多我自己整理过一个顺口溜方便考试策略模板观老者责任命令备状访迭代中介解释器。拆开看就是策略、模板方法、观察者、迭代器、责任链命令、备忘录、状态、访问者、中介者、解释器。背下来不难难的是理解哪些模式是高频面试王哪些是低频理论派。根据我刷面试题和软考真题的经验出现频率差异极大。策略、观察者、责任链、模板方法、状态这五个面试必考项目中也几乎天天见命令、迭代器、备忘录、中介者属于认识和场景匹配级别访问者和解释器在真实业务里极难见到面试也基本是概念题。所以后面几章的展开深度我会按这个频率分配——不是厚此薄彼而是把时间花在最容易变现的部分。2. 策略模式实战把算法抽离用到极致2.1 一个支付场景的演化过程先看一段最常见的祖传代码。public String pay(String payType, Order order) { if (wechat.equals(payType)) { // 调用微信支付的几十行代码 } else if (alipay.equals(payType)) { // 调用支付宝支付的几十行代码 } else if (card.equals(payType)) { // 调用银行卡支付的几十行代码 } else { throw new IllegalArgumentException(未知支付方式); } return ok; }这段代码的痛点很典型第一每次新增支付渠道都要打开订单支付方法动刀违反开闭原则的对扩展开放、对修改关闭第二支付渠道的细节和订单主流程耦合在同一方法里未来想给同一个订单加组合支付或支付渠道降级时这个方法的复杂度会指数级上升。策略模式正是为此而生——把每个支付渠道封装成独立的策略类主流程只面向策略接口编程。2.2 策略模式的标准结构策略模式有三个角色策略接口定义算法的统一入口具体策略类实现各自的算法上下文持有策略引用调用方把具体策略交给上下文。public interface PaymentStrategy { PayType getType(); void pay(Order order); }Service public class WechatPayStrategy implements PaymentStrategy { Override public PayType getType() { return PayType.WECHAT; } Override public void pay(Order order) { // 微信支付独有逻辑组装参数、调用SDK、处理回调 } }Service public class AlipayStrategy implements PaymentStrategy { Override public PayType getType() { return PayType.ALIPAY; } Override public void pay(Order order) { // 支付宝支付独有逻辑 } }2.3 配合Spring的工厂方案如果只写接口和实现类切换策略的职责就落在调用方身上调用方还是会有if-else。把Spring容器和策略模式结合才能把这些渠道类的注册、查找彻底隐藏。这里推荐自动注入列表 Map字典的写法Component public class PaymentStrategyFactory { private final MapPayType, PaymentStrategy strategyMap; public PaymentStrategyFactory(ListPaymentStrategy strategies) { this.strategyMap strategies.stream() .collect(Collectors.toMap(PaymentStrategy::getType, Function.identity())); } public PaymentStrategy get(PayType payType) { return Optional.ofNullable(strategyMap.get(payType)) .orElseThrow(() - new IllegalArgumentException(不支持的支付方式: payType)); } }调用方变成RestController public class OrderController { Resource private PaymentStrategyFactory strategyFactory; PostMapping(/pay) public String pay(RequestParam PayType payType, RequestBody Order order) { strategyFactory.get(payType).pay(order); return ok; } }Spring会收集容器中所有PaymentStrategy实现类自动注入到构造器参数ListPaymentStrategy里工厂初始化时转成Map。新增支付渠道时你只需要新增一个Service实现类不用改任何现有代码这就是开闭原则的直观体现。我在实际项目里用这招重构过聚合支付模块后来接入云闪付和数字人民币各花了不到半小时。2.4 策略组合与运行时切换搜索热词里出现java策略模式多种组合说明很多人卡在多个策略叠加怎么办上。策略模式也能组合比如支付前要计算实付金额会员折扣 满减 优惠券就是三个维度的算法叠加。你可以定义一个复合策略内部持有多个策略并依次执行Service public class CompositeDiscountStrategy implements DiscountStrategy { private final ListDiscountStrategy strategies; public CompositeDiscountStrategy(ListDiscountStrategy strategies) { this.strategies strategies; } Override public BigDecimal calculate(BigDecimal amount) { for (DiscountStrategy strategy : strategies) { amount strategy.calculate(amount); } return amount; } }运行时切换也是策略模式的核心卖点。用户切换支付方式、管理员切换限流算法、游戏里切换角色技能这些场景下上下文无需重启只要替换持有的策略引用即可。经验提醒策略类尽量设计成无状态不持有可变的成员变量。如果某个策略需要携带临时数据让它在方法内new局部变量或者把参数传进方法不要在多个线程之间共享同一个策略实例的可变状态否则排查并发问题时会非常痛苦。3. 观察者模式与责任链模式通知系统和审批流的骨架3.1 观察者模式支付成功后的联动动作订单支付成功后一般要同时做三四件事发短信、加积分、通知物流、更新推荐位。初版代码常写成串行调用public void handlePaySuccess(Order order) { smsService.send(order); pointService.add(order.getUserId(), order.getAmount()); logisticsService.notify(order); }下次新增一个支付后弹问卷你又得回到这个方法里加一行。更麻烦的是如果短信服务临时宕机整个支付成功链路会跟着超时。观察者模式的做法是反向设计支付成功只发布一个事件谁关心这个事件谁就去订阅业务之间不互相感知。public interface PaySuccessObserver { void onPaySuccess(Order order); }Component public class SmsPaySuccessObserver implements PaySuccessObserver { Override public void onPaySuccess(Order order) { // 发短信逻辑 } }Component public class OrderPayEventPublisher { private final ListPaySuccessObserver observers; public OrderPayEventPublisher(ListPaySuccessObserver observers) { this.observers observers; } public void publish(Order order) { observers.forEach(observer - observer.onPaySuccess(order)); } }ListPaySuccessObserver同样由Spring自动注入新增观察者零侵入。实际项目里如果希望观察者彼此不影响、且允许异步Spring的EventListener配合Async是更地道的做法Component public class LogisticsEventListener { Async EventListener public void handlePaySuccess(OrderPayEvent event) { // 异步通知物流失败不影响主链路 } }观察者模式最核心的价值是发布者和订阅者的解耦。发布者不需要认识任何订阅者订阅者增加、减少、替换都不影响发布者。这个语义和Spring Event、消息队列、响应式编程中的订阅模型一脉相承理解了观察者后面学这些都会顺很多。3.2 责任链模式从审批流到过滤器链责任链模式解决的是一个请求可能有多个对象依次尝试处理直到某个人接手为止的问题。最经典的场景是审批流请假天数不同审批人级别不同。public abstract class Approver { protected Approver next; public void setNext(Approver next) { this.next next; } public abstract void approve(int days); }public class ManagerApprover extends Approver { Override public void approve(int days) { if (days 3) { System.out.println(经理审批通过 days 天); } else if (next ! null) { next.approve(days); } } }public class DirectorApprover extends Approver { Override public void approve(int days) { if (days 7) { System.out.println(总监审批通过 days 天); } else if (next ! null) { next.approve(days); } } }调用方只需要组装链条Approver manager new ManagerApprover(); Approver director new DirectorApprover(); Approver boss new BossApprover(); manager.setNext(director); director.setNext(boss); manager.approve(5); // 经理不处理 - 总监处理责任链模式在Java生态里无处不在。Servlet里的Filter链、Spring MVC里的HandlerInterceptor本质就是责任链——每个过滤器决定放行还是中断。理解了这一点你再看Spring Security里那一长串过滤器就不会觉得是在背框架文档而是在理解一条经过设计的责任链。3.3 纯职责链与不纯职责链软考和面试偶尔会考这个概念。纯职责链要求一个请求要么被完全处理要么沿着链传递到末尾不允许一个节点处理一部分再往下传不纯职责链则允许能处理多少处理多少剩下的交给下一个比如日志框架里不同级别的日志由不同处理器处理后继续传递。日常开发中不纯职责链更实用过滤器的放行语义其实就是不纯职责链的变体。实际经验责任链的节点数量不宜过多。我见过有人把几十个校验规则都做成责任链节点结果链路太长请求耗时翻倍排查问题时像走迷宫。建议同类校验合并成一个节点链的总长度控制在七八个以内否则就要考虑用编排引擎替代了。4. 状态模式与模板方法把业务流程变成可维护的结构4.1 状态模式订单状态机的优雅写法订单有待支付、已支付、已发货、已完成、已取消这些状态每个状态下能做的操作不一样。原始写法是方法里套switchpublic void cancel(Order order) { switch (order.getState()) { case PENDING: // 取消逻辑 break; case PAID: // 退款 取消逻辑 break; case SHIPPED: // 需人工介入 break; default: throw new IllegalStateException(当前状态不可取消); } }状态一多每个操作都要写一个switch而且状态流转规则散落在各个方法里很容易出现在已取消状态还能发货这种bug。状态模式把状态本身建模成对象每个状态类决定自己允许哪些行为、流转到哪个下一个状态。public interface OrderState { void pay(OrderContext context); void ship(OrderContext context); void complete(OrderContext context); void cancel(OrderContext context); }public class PendingState implements OrderState { Override public void pay(OrderContext context) { // 执行支付逻辑 context.setState(new PaidState()); } Override public void ship(OrderContext context) { throw new IllegalStateException(待支付订单不能发货); } Override public void complete(OrderContext context) { throw new IllegalStateException(待支付订单不能完成); } Override public void cancel(OrderContext context) { // 取消逻辑 context.setState(new CancelledState()); } }订单上下文持有一个OrderState字段所有操作委托给当前状态对象。这样一来新增一个退款中状态只需要新增类改动的是状态转移表而不是翻开一堆switch逐个改。4.2 状态模式与策略模式的区别按我个人经验这是面试中打得最准的一个问题也是区分背过八股和真正理解的试金石。两个模式类结构几乎一样都有接口、实现类、上下文区别在于意图策略模式策略由调用方主动选择策略之间彼此独立、不具备流转关系。你选微信支付还是支付宝支付选完就结束了微信支付不会自动变成支付宝支付。状态模式状态由内部条件自动切换状态之间存在流转关系。待支付订单支付成功后就变成已支付状态这是业务规则驱动的必然流转。一句话总结策略是调用方选状态是自己变。如果你发现代码里某个策略接口的各个实现类之间竟然存在下一种情况的转移关系那它大概率应该改成状态模式。4.3 模板方法固定流程与可变步骤的分离模板方法模式的核心是算法骨架放在父类且不让子类修改不同步骤的细节交给子类实现。最典型的应用是数据导入功能。不管导入Excel、CSV还是第三方接口数据流程本质上都是读取 - 解析 - 校验 - 入库但每一步的具体实现完全不同。public abstract class DataImporter { // final修饰防止子类修改流程骨架 public final void importData(String source) { ListString rawData read(source); ListRecord records parse(rawData); validate(records); save(records); } protected abstract ListString read(String source); protected abstract ListRecord parse(ListString rawData); // 钩子方法允许子类覆盖但非强制 protected void validate(ListRecord records) { // 默认不校验 } protected abstract void save(ListRecord records); }public class ExcelImporter extends DataImporter { Override protected ListString read(String source) { // 读Excel } Override protected ListRecord parse(ListString rawData) { // 解析Excel行 } Override protected void validate(ListRecord records) { // Excel专属校验 } Override protected void save(ListRecord records) { // 批量入库 } }模板方法和策略模式都强调把变化隔离区别在于模板方法用继承实现变与不变的分离策略模式用组合。Java标准库里到处是模板方法——AbstractList里add()抛异常让子类按需覆盖AbstractQueuedSynchronizer把加锁骨架固定把是否允许获取锁留给子类判断。Spring中的JdbcTemplate、RestTemplate也是模板方法思想的体现。4.4 模板方法的实战纪律模板方法有一点容易烂尾父类骨架一旦定了子类之间的差异只能靠覆盖钩子方法消化如果差异太大子类就会大面积重写骨架失去约束意义。我自己的判断标准是如果某个子类重写了父类一半以上的方法说明这个模板抽得不对应该拆成两个独立的模板中间的公共部分再考虑用组合方式下沉。5. 剩余六种模式怎么理解命令、迭代器、中介者、备忘录、访问者、解释器5.1 命令模式把请求封装为对象命令模式把发起请求的调用方和执行请求的接收方彻底分离中间多了一个命令对象。最经典的场景是编辑器的撤销重做——每个操作都是实现同一个接口的命令对象execute()和undo()把命令放进历史栈执行后压栈撤销时弹栈并调用undo()。public interface Command { void execute(); void undo(); }除了撤销命令模式还适合把操作排队、记录日志、延迟执行。在Java里Runnable其实就可以看作一种命令对象——把一段逻辑封装起来交给线程池调度。理解命令模式后你会明白Java里大量接口 匿名内部类/lambda的写法背后都有命令模式的影子。5.2 迭代器模式Java集合的基石迭代器模式可能是你最早接触、却最无感知的一个模式。ArrayList、HashSet内部结构完全不同但都可以用Iterator统一遍历这就是迭代器模式的价值——提供一种顺序访问聚合对象内部元素的方法又不暴露内部表示。Java的foreach语法糖底层就是IteratorListString list Arrays.asList(a, b, c); for (String s : list) { System.out.println(s); }面试中偶尔会问for循环删除元素为什么会抛ConcurrentModificationException答案也和迭代器有关——ArrayList迭代器的next()方法会校验modCount集合结构被修改后modCount变化迭代器就会主动失败。这是迭代器模式设计时防范并发修改的手段。5.3 中介者模式迪米特法则的实践多个对象互相直接通信会形成网状结构任何一个对象变化都要牵动其他对象。中介者模式引入一个调度中心把网状通信变成星形通信——对象之间不再互相认识只和中介者通信。聊天室的服务器、机场的塔台都是生活中的中介者。Java GUI开发里中介者用得很多多个按钮、输入框、列表共享同一个控制器由控制器统一协调它们之间的联动避免每个控件都持有其他控件的引用。面试时知道中介者解决多对多复杂交互就够了。5.4 备忘录模式撤销功能的实现备忘录模式解决如何保存和恢复对象的历史状态——把状态快照封装成备忘录对象由负责人Caretaker保存不破坏对象的封装性。文本编辑器的撤销、游戏的存档点都是它的应用场景。用的时候要注意内存开销。如果对象很大、操作频繁每次都做全量快照会很浪费资源实际项目里常用增量快照或memento command组合命令负责记录操作差异备忘录负责定点快照。5.5 访问者模式数据结构与操作的分离访问者模式最难理解但应用场景特别清晰对象结构稳定但希望在不修改结构类的前提下增加新操作。比如一个语法树树的节点类型基本固定表达式节点、变量节点、字面量节点但你要给它增加代码生成类型检查格式化等操作。用访问者模式每种操作实现一个Visitor逐个访问节点。Java里最典型的例子是javax.lang.model.element.ElementVisitor编译期的注解处理器用它遍历语法树Spring的BeanDefinitionVisitor也类似。代价是往结构中增加新节点类型很麻烦所以结构稳定是使用它的前提。5.6 解释器模式存在感最低的模式解释器模式定义一门语言的文法并建立解释器来解释句子。最经典的例子是计算器定义数字、加法、乘法这些表达式类组合起来求值。但现实中你几乎不会手写一套解释器——有了ANTLR、表达式引擎Aviator、QLExpress和脚本引擎解释器模式的实现成本远高于收益。我刷软考题见到它的频率不低但在这个项目级代码里这11个模式中它确实是最容易只是听说过的一个。6. 面试和软考中高频的混淆点拆解6.1 状态模式 vs 策略模式前面第4章已经拆过这里把判据再拧一遍。考场上拿到一道题如果一个类有几个实现类且实现类之间的差异是调用方在运行时替换一套算法选策略如果差异是对象内部状态导致行为不同且行为发生后状态还会自动改变选状态。所有状态机题基本都选状态算法替换题基本都选策略。6.2 观察者模式 vs 中介者模式这两个都解决对象之间的通信但方向完全不同。观察者是一对多广播被观察者只发出通知不关心谁在听中介者是多对多协调所有对象都把消息发给中介者由中介者决定转发给谁。电商系统的消息通知、事件驱动架构用观察者聊天室、GUI联动、微服务间的编排协调用中介者。6.3 命令模式 vs 策略模式很多人容易混这两个因为类结构都是接口 实现类 调用方。区别在意图上策略模式把做事的算法封装成可替换对象关注怎么做命令模式把做的事情本身封装成对象关注做什么以及如何撤销。策略的调用方结果通常是一个值或一个状态命令的调用方结果往往是某个操作被记录并执行了。6.4 责任链模式 vs 装饰器模式结构型里的装饰器模式容易和责任链搞混因为它们都有链式引用。区别在于执行方式装饰器是层层增强请求一定走完整个装饰链责任链是依次尝试某个节点处理后可能直接返回链可能中途断裂。日志逻辑、权限校验适用责任链给流加缓存、给对象加序列化能力适用装饰器。6.5 软考高频速记表题目问法答案模式算法封装、可替换、消除条件分支策略模式算法骨架固定子类实现细节模板方法模式一对多通知、发布订阅观察者模式多个处理器依次尝试处理请求责任链模式对象状态不同、行为不同且状态可流转状态模式请求封装为对象支持撤销/排队命令模式不暴露内部结构顺序访问一个聚合对象迭代器模式集中控制多个对象的交互、降低耦合中介者模式保存并恢复对象历史状态备忘录模式不修改结构的前提下增加新操作访问者模式定义文法并解释语言解释器模式看完这张表再回去看那六种低频模式其实每个人都对应一颗钉子考题问法就是那把锤子。只要能把场景和模式名称对上拿分并不难。7. 选型与落地行为型模式在实际项目里怎么用7.1 一个业务方法该不该用设计模式写过几年代码后我养成了一个习惯看到一个方法里有三个以上的if-else分支先不问用什么模式而是问这几个分支是为什么而变。如果变的原因只有一个维度——比如支付渠道、导入格式、折扣算法——那策略或模板方法通常就是对的答案如果变的原因是多个维度叠加先考虑能否用Map索引消除分支再考虑组合策略如果代码本身稳定、需求变化很少那哪怕分支多一点也不必硬套模式。设计模式的核心价值是隔离变化点。没有变化点的地方套模式只会把简单问题复杂化。判断有没有变化点最直接的办法是问自己未来一个月内这个分支会不会新增一种如果答案是不确定就先保持简单等第二个同类分支出现再重构。这是我踩过不少次坑以后才接受的观念——过度设计也是一笔负债。7.2 渐进式重构的推荐路线如果决定要用策略模式或状态模式重构现有代码我建议走垫石子路线而不是一次性大翻修。第一步先把if-else里的每个分支抽成独立的私有方法确保行为不变。第二步把每个私有方法调整为策略类或状态类的公有方法用临时Map接入。第三步观察运行稳定后再把每个分支里重复的样板代码下沉到抽象类或模板方法。每一步都能独立上线、独立回滚比一个巨大PR把几百处改动一次合入要稳妥得多。我在重构支付模块时的顺序就是先抽私有方法 - 再抽策略类 - 最后引入工厂 Spring自动注入。每一步都保持原有测试通过整个重构大概花了两周的下班时间没有一次线上事故。7.3 在框架里反推学习Spring和JDK本身就是行为型模式的最佳教案。看JdbcTemplate学模板方法看ApplicationEventPublisher学观察者看HandlerInterceptor学责任链看ThreadPoolExecutor的任务提交学命令模式。每用一个框架机制就去框架源码里找一下结构对应的模式比死记硬背要牢固得多。7.4 一个容易实现的个人项目练习如果你刚学完行为型模式想找个小项目练手我强烈推荐自己做一个多策略的订单履约引擎订单创建后走校验策略链、支付走支付策略组、状态流转走状态机、成功之后发事件。这四个环节正好覆盖责任链、策略、状态、观察者四个最常考的模式。做完这个你对行为型模式的理解会超过绝大多数只会背概念的候选人。最后再分享一个实际心得。设计模式不是考试结束就扔掉的八股文而是当你写代码时心里能多出的那几种组织对象的直觉。遇到业务变化就主动问一句这里的变化能不能被我正在设计的那一层结构吸收掉如果能说明模式用对了如果不能别硬套先让代码保持朴素。行为型模式最大的价值不是让你的代码看起来高级而是当需求真的变起来的时候你能少改几行代码多睡几个安稳觉。
返回列表