多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

设计模式 14 · 策略模式

设计模式 14 · 策略模式 从这一篇开始,我们进入设计模式里数量最多、也最精彩的一类——行为型模式。前面两大类各有侧重:创建型管对象怎么造出来,结构型管对象怎么组合连接,而行为型管的是**“对象之间如何协作、职责如何分配、算法如何切换”——它关注的是运行时的行为流转**。行为型有十一个模式,我们从其中最实用、也最好懂的一个开场:策略模式(Strategy)。策略模式要解决的问题,你几乎天天遇到:同一件事,有多种不同的做法(算法),需要根据情况选用其中一种,而且以后还可能增加新做法。最典型的信号,就是代码里出现了一大坨if-else或switch在根据某个条件,选择不同的处理逻辑。我们的订单场景里就有一个绝佳例子——计算运费:包邮、按重量计费、按距离计费、偏远地区加价……不同的计费方式就是不同的策略。我们在第 3 篇工厂方法里其实已经和策略打过照面了——当时说消灭 if-else 的是策略,而不是工厂。这一篇就正式把它讲透:策略模式如何把那坨丑陋的if-else拆解成一个个独立、可替换、可扩展的策略类。你会发现它的结构极其简单,简单到你可能已经在无意中用过它了;但把它的意图、和工厂/状态模式的界限讲清楚,才能真正用好它。这篇文章按这条线索展开:先看运费计算那坨if-else有多难维护;再引出策略模式如何拆解它;然后讲清它的角色、以及谁来选策略这个关键问题;接着看 Java 里天天用的策略(Comparator、Lambda);最后辨析它与工厂、状态的区别,给出适用边界。贯穿例是运费计算。目录一坨运费 if-else 的困境策略模式:把每种算法拆成独立策略角色,以及谁来选策略这个关键问题你天天在用的策略:Comparator 与 Lambda策略 vs 工厂 vs 状态,以及什么时候用一、一坨运费 if-else 的困境看需求。订单结算时要算运费,规则有好几种:publicclassShippingService{publicdoublecalculate(Stringtype,doubleweight,doubledistance){if(type.equals(free)){// 包邮return0;}elseif(type.equals(weight)){// 按重量:每公斤 5 元returnweight*5;}elseif(type.equals(distance)){// 按距离:每公里 0.5 元returndistance*0.5;}elseif(type.equals(remote)){// 偏远地区:按距离 固定加价returndistance*0.820;}thrownewIllegalArgumentException(未知运费类型);}}这段代码能跑,但它的毛病和第 3 篇支付那坨if-else如出一辙:违反开闭原则:每加一种计费规则(比如促销免运费门槛),你都得回来撬开这个方法,加一个else if。这个方法会越长越大,变成一个谁都不敢碰的雷区。职责堆积:所有计费算法都挤在一个方法里,ShippingService什么都管,违反单一职责。一处算法的改动,有可能波及整个方法。难以复用和测试:想单独测按重量计费这段逻辑,你没法把它单独拎出来——它被埋在if-else的分支里。问题的根源是:多种平行的算法被硬编码、堆砌在了一个方法的if-else分支里。这些算法本是彼此独立、可以自由增减的,却被if-else焊成了一坨。我们真正想要的是:把每种算法单独拎出来,做成一个独立的、可插拔的东西,用哪个就装哪个,加新算法不影响旧的。这就是策略模式。二、策略模式:把每种算法拆成独立策略策略模式的做法:定义一个策略接口,把每一种算法实现成一个独立的策略类;调用方持有一个策略接口的引用,具体用哪个策略在运行时决定。第一步,定义策略接口:// 策略接口:所有运费算法的共同抽象publicinterfaceShippingStrategy{doublecalculate(doubleweight,doubledistance);}第二步,每种计费规则是一个独立的策略实现:publicclassFreeShippingimplementsShippingStrategy{publicdoublecalculate(doublew,doubled){return0;}}publicclassWeightShippingimplementsShippingStrategy{publicdoublecalculate(doublew,doubled){returnw*5;}}publicclassDistanceShippingimplementsShippingStrategy{publicdoublecalculate(doublew,doubled){returnd*0.5;}}publicclassRemoteShippingimplementsShippingStrategy{publicdoublecalculate(doublew,doubled){returnd*0.820;}}第三步,调用方(上下文)持有一个策略,把计算委托给它:publicclassShippingService{privateShippingStrategystrategy;// 持有一个策略publicvoidsetStrategy(ShippingStrategystrategy){// 运行时可替换this.strategystrategy;}publicdoublecalculate(doubleweight,doubledistance){returnstrategy.calculate(weight,distance);// 委托给策略,自己不做判断}}用起来,想用哪种算法就装哪种,if-else消失了:ShippingServiceservicenewShippingService();service.setStrategy(newWeightShipping());// 这单按重量System.out.println(service.calculate(3,100));// 15service.setStrategy(newDistanceShipping());// 换个策略,按距离System.out.println(service.calculate(3,100));// 50对比第一节,升级点非常清晰:那坨if-else被拆成了一个个独立的策略类,ShippingService里再也没有类型判断。要加一种新计费规则?新建一个implements ShippingStrategy的类就行,ShippingService和其他策略一个字都不用改——完美符合开闭原则。每种算法各自独立,可以单独复用、单独测试。这正是策略模式的核心价值:用多态替换条件分支,把变化的算法隔离成可插拔的策略。三、角色,以及谁来选策略这个关键问题策略模式的角色很简单,三个:角色本例中是谁职责策略接口(Strategy)ShippingStrategy定义所有算法的公共接口具体策略(ConcreteStrategy)WeightShipping等各自实现一种算法上下文(Context)ShippingService持有策略引用,把请求委托给它结构讲完了,但有一个绕不开的问题必须面对——你可能已经发现了:第 2 节里,那坨if-else好像并没有真正消失,只是从ShippingService里,转移到了决定new哪个策略的地方。谁来决定用WeightShipping还是DistanceShipping?这个选择的逻辑,总得有个地方做。这是理解策略模式最关键的一点:策略模式本身,只负责把算法封装成可替换的策略,它并不负责怎么选策略。选哪个策略这件事,通常有几种解法:由客户端直接选:调用方自己new一个具体策略传进去(像第 2 节那样)。简单直接,适合调用方明确知道该用哪个的场景。用工厂来选:写一个ShippingStrategyFactory,根据类型返回对应策略——这正是策略 工厂的经典组合。选择逻辑收拢到工厂里,if-else或Map都在工厂内部。用 Map 注册表消灭 if-else:把type → 策略的映射放进一个Map,用 key 直接取策略,连if-else都省了:// 用 Map 彻底消灭选择时的 if-elseprivatestaticfinalMapString,ShippingStrategySTRATEGIESMap.of(free,newFreeShipping(),weight,newWeightShipping(),distance,newDistanceShipping(),remote,newRemoteShipping());publicdoublecalculate(Stringtype,doubleweight,doubledistance){ShippingStrategystrategySTRATEGIES.get(type);// 一句话取策略,零判断if(strategynull)thrownewIllegalArgumentException(未知运费类型);returnstrategy.calculate(weight,distance);}在 Spring 项目里,这个 Map 甚至可以自动装配:把所有策略都声明成 Bean,Spring 能自动把它们收集进一个MapString, ShippingStrategy(key 是 Bean 名),新增策略只要加一个Component,注册表自动更新,真正做到加算法零改动。所以要诚实地说:策略模式没有凭空消灭if-else,而是把算法逻辑和选择逻辑解耦了——算法逻辑被拆进各个策略类(彻底可扩展),选择逻辑被收拢到一处(工厂或 Map)。这个解耦本身就是巨大的进步:算法部分完全符合开闭原则,而选择部分即便还有一点判断,也集中、清晰、好维护。别指望模式变魔术;它的价值是把混在一起的东西分开,而不是让东西凭空消失。四、你天天在用的策略:Comparator 与 Lambda策略模式可能是你最常用却最没意识到的模式,因为 Java 里到处都是它:Comparator:这是策略模式教科书级的例子。Collections.sort(list, comparator)里,排序算法是固定的,但怎么比较两个元素这个策略,由你传入的Comparator决定。按价格排、按时间排、按名称排——每个Comparator就是一个具体策略,sort是上下文。你换一个Comparator,就换了一种排序策略,而sort本身不用改。Comparator Lambda:自从有了 Lambda,策略模式的写法被极大简化了。你不用再为每个策略写一个类,直接用 Lambda 当策略:// Lambda 就是一个轻量级策略,不用单独建类orders.sort((a,b)-Double.compare(a.getAmount(),b.getAmount()));// 按金额排orders.sort(Comparator.comparing(Order::getCreateTime));// 按时间排函数式接口 Lambda,本质上就是策略模式的语法糖。每一个 Lambda 表达式,就是一个即写即用的匿名策略。所以在现代 Java 里,当策略很简单(就一个方法、逻辑几行)时,直接用 Lambda,不必大动干戈地建一堆策略类——这是策略模式在 Java 8 之后最自然的用法。其他还有ThreadPoolExecutor的拒绝策略(RejectedExecutionHandler,如AbortPolicy、CallerRunsPolicy)、Spring 的Resource加载策略等,都是策略模式。五、策略 vs 工厂 vs 状态,以及什么时候用策略容易和两个模式混,辨析一下:策略 vs 工厂:两者常配合使用,但职责不同。工厂负责创建对象(造哪个策略),策略负责行为(算法本身)。工厂关心给你一个对象,策略关心这个对象怎么干活。第三节的策略 工厂组合,正是让工厂来解决策略的选择问题。策略 vs 状态(后面会讲):两者结构几乎完全一样(都是上下文持有一个接口引用、委托调用),这是最容易混的一对。区别在意图:策略的各个算法是平行、独立的,它们之间没有关系,由外部选择用哪个(选运费算法,和上一个用什么无关);状态的各个状态是有关联、会流转的,一个状态会决定下一个状态是什么,是内部驱动的转换(订单从待付款流转到已付款)。一句话:策略是平行选一个,状态是链式流转。到状态模式那篇我们会再夯实这条界限。适合用策略的信号:一件事有多种平行的算法/做法,需要在运行时选用其一;代码里出现了根据类型选择不同处理逻辑的if-else/switch,且这些分支未来会增减;你希望每种算法能独立扩展、复用、测试。不必用的信号:就两三个分支,而且永远不会再增加——那简单的if-else反而更直白,套策略是过度设计;算法极其简单(一行)且用一次——直接写、或用 Lambda,不必建策略类。判断的核心还是那句话:先确认真的存在多种会增减的平行算法,策略才值得上。给一个固定的两分支判断硬套策略模式,凭空多出接口和一堆类,是典型的过度设计。而且现代 Java 里,简单策略优先用 Lambda,别一上来就是一堆策略类。小结。策略模式专治多种平行算法用if-else硬编码的困境:把每种算法封装成独立的策略类,上下文持有策略引用、委托调用,用多态替换条件分支,让算法可插拔、可扩展、符合开闭原则。要清醒地认识到,它不是凭空消灭if-else,而是把**“算法逻辑和选择逻辑解耦**——算法拆进策略类,选择收拢到工厂或 Map 注册表(Spring 里还能自动装配)。Comparator和 Lambda 是你天天在用的策略,现代 Java 里简单策略直接用 Lambda 即可。记住它和状态的分水岭:策略是平行选一个,状态是链式流转。下一篇我们讲模板方法模式——如果说策略是把整个算法换掉”,那模板方法就是算法的大骨架固定不变,只把其中几个步骤留给子类去填。
返回列表