如果有人问你什么是策略模式?你可以尝试这样回答
策略模式是一种行为设计模式,它允许在运行时根据不同的情况选择不同的算法策略。这种模式将算法的定义与使用的代码分离开来,使得代码更加可读、可维护和可扩展。
在策略模式中,通常有一个抽象的策略接口,定义了一系列可以被不同策略实现类所实现的方法。在使用时,可以根据需要选择合适的具体策略类,并通过接口进行调用。
虽然解释了策略模式,但是面试官可能会认为你在吊书袋,完全没有自己的理解。我因为被领导卷到了,所以对策略模式有一些其他的理解,接下来我从业务中台实际遇到的问题出发,谈谈怎么被领导卷到,谈谈我对策略模式的优化和理解。
在业务中台,可以根据具体情况选择合适的策略类来执行相应的业务逻辑,从而满足不同业务方的要求。
由于各个业务方的业务逻辑存在差异性和相似性,编写中台代码时需要考虑这些差异性,并预留扩展点以供不同业务方根据自身需求提供策略类。在这种情况下,可以应用策略模式。
当业务方有特殊的业务逻辑时,只需要添加新的策略实现类即可,不需要修改现有的代码,方便了后续的维护和扩展。
然而在我们在应用策略模式时遇到了几个问题
遇到的实际问题
由于业务中台逻辑非常复杂,每个业务线的业务场景都很难保证完全一样,在代码实践中我们的系统出现了非常庞大复杂的策略类,将大量业务逻辑整合到同一个策略中,这导致系统能力复用非常困难。
举个例子,在退款校验逻辑中 业务场景 A,(售卖红包,类似于美团饿了么会员)。 需要判断如下校验逻辑,如果命中,则不允许退款!
-
订单是否在生效期,过期不允许退
-
订单是否在已使用,已使用不可退
-
订单如果是某类特殊商品订单,则不可退
-
如果超过当天最大退款次数,则不可退。
于是我们在编写代码时,就新定义 ConcreteStrategyA ,将以上 4 个业务逻辑放到策略类中。
过一段时间,又出现了业务场景 B(售卖红包,类似于美团饿了么会员)。它的校验逻辑和A 大同小异
-
订单是否在生效期,过期不允许退
-
订单售卖的红包完全使用不可退,部分使用可以退。
-
如果超过当天最大退款次数,则不可退。
业务场景B 相比A 而言,少校验了 “特殊商品订单不可退”的逻辑,同时增加了部分使用可以退的逻辑。
如何设计代码实现两种业务场景的校验逻辑呢? 这是非常具体的问题,如果是你如何实现呢?
完全独立的策略类
我们在最开始写代码时,分别独立实现了 ConcreteStrategyA、ConcreteStrategyB。两者的校验逻辑各自独立,没有代码复用。
此时的系统类图如下
这种实现方式是大量的代码拷贝,例如退款次数限制、生效期校验等代码都大量重复拷贝了。
后来我们发现了这个问题,将相关校验方法抽到父类中。
继承共同的父类
为了更好的复用校验策略,我们将校验生效期的方法、校验退款次数的方法抽取到共同的父级策略类。由具体的子策略类继承BaseStrategy 父策略类,在校验逻辑中,使用父级的校验方法。
如下图的类图所示
在相当长的一段时间里,我们认为这已经是最优的实现方式了。
但是被领导卷到了!
被卷到了
“新增业务场景时,为什么校验逻辑都是复用的原有能力,还需要新增扩展类,还需要开发代码呢?” 领导这样问道。
我尝试回答领导的问题:“开发这段代码,并不算太难,只需要增加一个扩展类就可以了!”
“你数数现在有多少个扩展类了?” 领导似乎有些生气,
我一看扩展类的数量,被吓到了,已经有了15个扩展类。这一刻真的非常尴尬,平时领导从不亲自看代码。估计是突然心血来潮。从表情来看,他好像很生气,估计是代码没看懂!
“我看到这部分代码时,想要查看具体的策略类,但Idea直接刷出了15个策略类……为什么会有这么多的扩展类呢?” 领导进一步补充道。
场面僵住了,我也没什么好办法……。当然领导向来的传统是:只提问题,不给解决办法! 我只能自己想解决办法!如何解决策略类过多、扩展类膨胀的问题呢?
策略类过多怎么办!
当业务场景非常多,业务逻辑非常复杂时,确实会出现非常多的策略类。但领导的问题是,现有业务场景有很多相似性,某些新增业务场景和原有业务场景类似,校验逻辑也是类似的,为什么还需要新增扩展类呢?
经过深思熟虑,让我发现问题所在!
策略类粒度太粗,导致系统复用难
目前我们的系统设计是每个业务场景都有单独的策略,这是大多数人认可的做法。不同的业务场景需要不同的策略。
然而,仔细思考一下我们会发现,不同业务场景的退款校验逻辑实际上是由一系列细分校验逻辑组合而成的,退款校验逻辑在不同的业务场景中是可以被重复使用的。
为什么不能将这些细分退款校验逻辑抽象为策略呢?例如,将过期不允许退款、已生效不可退款、超过最大退款次数不允许退款等校验逻辑抽象成为校验策略类。
各个业务场景组合多个校验策略,这样新增业务场景时,只需要组合多个校验策略即可。
如何组合校验策略
首先需要抽象校验策略接口: VerifyStrategy
classDiagram
class VerifyStrategy{
+void verify(VerifyContext context);
}
然后定义 VerifyScene 类
classDiagram
class VerifyScene{
+ Biz biz;
+ List<VerifyStrategy> strategies;
}
如何把对应具体策略类配置到VerifyScene中呢?本着 ”能不开发代码,就不要开发代码,优先使用配置!“的原则,我们选择使用Spring XML 文件配置。(在业务中台优先使用配置而非硬编码,否则这个问题不好回答。“业务方和中台都需要开发,为啥走你中台?”)
使用Spring XML 组合校验策略
在Spring XML文件中,可以声明VerifyScene
类的Bean,初始化属性时,可以引用相关的校验策略。
<bean name="Biz_A_Strategy" p:biz="A" class="com.XX.VerifyScene">
<property name="strategies">
<list>
<ref bean="checkPeriodVerifyStrategy"/> <!--校验是否未过期-->
<ref bean="checkUsageInfoVerifyStrategy"/> <!--校验使用情况-->
<ref bean="checkRefundTimeVerifyStrategy"/><!--校验退款次数-->
</list>
</property>
</bean>
当需要新增业务场景时,首先需要评估现有的校验策略是否满足需求,不满足则新增策略。最终在XML文档中增加 VerifyScene
校验场景,引用相关的策略类。
这样新增业务场景时,只要校验逻辑是复用的,就无需新增扩展类,也无需开发代码,只需要在XML中配置策略组合即可。
在XML文档中可以添加注释,说明当前业务场景每一个校验单元的业务逻辑。在某种程度上,这个XML文档就是所有业务的退款校验的业务文档。甚至无需再写文档说明每个业务场景的退款策略如何如何~
和领导汇报以后,领导很是满意。对业务方开始宣称,我们的中台系统支持零开发,配置化接入退款能力。
结束了吗?没有 ,我们后来想到更加优雅的方式。
使用Spring Configuration 和 Lamada
Spring 提供了@Bean注解注入Bean,我们可以使用Spring @Bean方式声明校验策略类
@Bean
public VerifyStrategy checkPeriodVerifyStrategy(){
return (context)->{
//校验生效期
};
}
通过以上方式,可以把checkPeriodVerifyStrategy 校验策略注入到Spring中,spring beanName就是方法名checkPeriodVerifyStrategy。
在Spring XML中可以使用 <ref bean="checkPeriodVerifyStrategy"/>
引用这个bean。
并且当点击XML中beanName时,可以直接跳转到 被@Bean修饰的checkPeriodVerifyStrategy方法。这样在梳理校验流程时,可以很方便地查看代码
点击这个BeanName,会跳转到对应的方法。(付费版Idea支持,社区版 Idea 不支持这个特性)
总结
总结几个问题
-
策略模式目的是:根据不同的业务场景选择不同的策略来执行相应的逻辑
-
策略模式一定要进行细化,通过组合多个细分策略模式为一个更大的策略,避免使用继承方案。
-
使用Spring XML 组合多个策略模式,可以避免开发。减少新增策略类
-
使用Spring Configuration @Bean 将策略类注入Spring 更加优雅