第21章 Spring事务管理之扩展篇
21.2 谈Strategy模式在开发过程中的应用
Strategy模式的本意是封装一系列可以互相替换的算法逻辑,使得具体算法的演化独立于使用它们的客户端代码。 为了理解为什么要这么做,我们不妨来看如下这个具体的场景。
在一个信贷系统中,通常会提供多种还款方式,比如等额本金还款方式、等额本息还款方式、一次还本付息方式等,那么,针对每位顾客所选择的还款方式,我们就需要按照这些还款方式的具体逻辑,为顾客计算每次所需要归还的本金以及利息的额度。如果要我们来实现这个根据还款方式计算额度的逻辑,我们会怎么做呢?
对于谙熟结构化编程或者面向对象编程不甚娴熟的开发人员来说,他们可能会直接使用多重条件语句来实现这段计算逻辑,如下方代码清单所示。
public RepaymentDetails calculateRepayment(BigDecimal tota1Amount, String customerId) {
RepaymentDetails details = new RepaymentDetails();
Object type = getRepaymentTypeByCustomerId(customerId);
if(isEqualInterestRepaymentType(type)) {
BigDecimal interest = getEqualInterestOfCentrelBank();
YearMonthDay repaymentInterval = getRepaymentIntervalByCustomerId(customerId);
// 根据totalAmout和其他数据执行计算
}
if(isEqualPrincipalRepaymentType(type)) {
BigDecimal interest = getStandardInterestOfCentrelBank();
YearMonthDay repaymentInterval = getRepaymentIntervalByCustomerId(customerId);
// 根据totalAmout和其他数据执行计算
}
if(isOnceForAll(type)) {
BigDecimal interest = getStandardInterestOfCentre1Bank();
// 根据totalAmout和其他数据执行计算
}
...
return details;
}
当然,我们可以对这些代码做进一步的改进,但是,如果总体结构上不做任何变更的话,这种实现方式的问题会依然存在:
-
客户端代码与算法逻辑代码相互混杂,导致客户端代码过于复杂并且后期难以维护;
-
混杂的算法逻辑代码与客户端代码耦合性太强 ,算法的变更或者添加新的算法都会直接导致客户端代码的调整,使得客户端代码和算法逻辑代码无法独立演化;
-
几乎同一逻辑单元内实现的各种算法,无可避免地需要多重的条件语句来区分针对不同算法所使用的数据或者对应算法的特定逻辑实现。
所以,该是Strategy模式登场的时间啦!
使用Strategy模式来重构这段代码的话, 我们首先通过RepaymentStrategy定义来抽象还款逻辑算法,然后,针对不同的还款方式,给出RepaymentStrategy定义的不同实现。对于使用还款逻辑的客户端代码来说,它只需要获取相应的RepaymentStrategy引用,并调用接口公开的计算接口即可。
整个图景如图21-3所示。
客户端代码只需要跟策略接口打交道,而算法的变更以及添加,对于使用策略接口进行计算操作的客户端代码来说,几乎没有任何影响。
虽然Strategy模式定义上强调的是对算法的封装,但我们不应该只着眼“算法”一词。实际上,只要能够有效地 剥离客户端代码与特定关注点之间的依赖关系 ,Strategy模式就应该进入考虑之列。在这一点上,Spring框架的事务抽象就是一个很好的范例。 通过将使用不同事务管理API进行事务管理的界定行为进行统一的抽象,客户端代码可以以透明的方式使用PlatformTransactionManager这一策略接口进行事务界定 ,即使具体的事务策略需要变更,对于客户端代码来说也不会造成过大的冲击。
Spring框架中使用Strategy模式的地方很多,除了本部分的事务抽象框架,还包括以下几处。
-
在IoC容器根据bean定义的内容,实例化相应bean对象的时候,会根据情况决定使用反射还是使用CGLIB来实例化相应的对象。 InstantiationStrategy是容器使用的实例化策略的抽象接口,Spring框架默认提供了CglibSubclassingInstantiationStrategy和SimpleInstantiationStrategy两个具体实现类。
-
Spring的Validation框架中,org.springframework.validation.Validator定义也是一个策略接口, 具体的实现类将根据具体场景提供不同的验证逻辑。 而这些具体验证逻辑的差异性,对于使用validator进行数据验证的客户端代码来说,则是透明的。
除了在Spring框架内会发现Strategy模式的大量使用,我们也可以在其他的框架设计中发现Strategy模式的影子,比如最常用的jakartacommonslogging中,Log接口就是一个策略接口,Jdk14Logger、Log4JLogger以及SimpleLog等都是具体的策略实现类。
可见,只要针对同一件事情有多种选择的时候,我们都可以考虑用Strategy模式来统一一下抽象接口,为客户端代码“造福”。
Strategy模式的重点在于通过统一的抽象,向客户端屏蔽其所依赖的具体行为,但该模式并没有关注客户端代码应该如何来使用这些行为。 一般来讲,客户端代码使用Strategy模式的方式可以简单划分为如下两种。
-
客户端整个生命周期内只依赖于单一的策略。 Spring提供的事务抽象可以归属这一类情况。使用PlatformTransactionManager进行事务界定的客户端代码,在其整个生命周期内只依赖于一个PlatformTransactionManager的实现类,或者DataSourceTransactionManager的实现类,或者HibernateTransactionManager的实现类等,这样的情况比较容易处理,直接为客户端代码注入所需要的策略实现类即可。
-
客户端整个生命周期内可能动态依赖多个策略。 比如我们的还款场景中,客户端可能需要根据每个顾客选择的还款方式,来决定使用哪个策略实现类为其计算对应的还款明细。对于这种情况,你会发现,Strategy模式通常宣称的可以避免多重条件语句的问题,其实只是将其转移给了客户端代码而已(见下方代码清单)。
RepaymentStrategy strategy = fallbackStrategy(); … public RepaymentDetails calculateRepayment(BigDecimal totalAmount, String customerId) { Object type = getRepaymentTypeByCustomerId(customerId); if(isEqua1InterestRepaymentType(type)) { strategy = EqualInterestStrategy(); } if(isEqualPrincipalRepaymentType(type)) { strategy = EqualPrincipalStrategy(); } if(isOnceForAll(type)) { strategy = OnceForAl1Strategy(); } … return strategy.performCalculation(); }
不过,如果我们想真正地避免多重条件语句的话,也不是没有办法。 最简单的方法就是提前准备一个具体策略类型与其对应条件之间的关系映射。
对于还款的场景来说,我们可以这么做。
(1)在客户端代码中声明一一个对关系映射的依赖,如以下代码所示:
public class StrategyContext {
private Map<Object, RepaymentStrategy> strategyMapping;
...
// setter和getter方法
}
(2)通过IoC容器注入所有客户端可能动态依赖的策略实现类实例(见下方代码清单)。
<bean id="strategyContext" class="...StrategyContext">
<property name="strategyMapping">
<ref local="strategyMapping"/>
</property>
</bean>
<util:map id="strategyMapping">
<entry key="EQAUL_INTEREST">
<bean class="...EqualInterestStrategy"></bean>
</entry>
<entry key="EQAUL_PRINCIPAL">
<bean class="...EqualPrincipalStrategy"></bean>
</entry>
<entry key="ONCE_FOR_ALL">
<bean class="...OnceForAllStrategy"></bean>
</entxy>
</uti1:map>
(3)在计算还款明细的方法中,使用还款策略的代码直接从关系映射中获取具体的策略即可,如下方代码清单所示。
RepaymentStrategy strategy = fallbackStrategy();
public RepaymentDetails calculateRepayment(BigDecimal totalAmount, String customerId) {
Object type = getRepaymentTypeByCustomerId(customerId);
RepaymentStrategy strategy = strategyMapping.get(type);
// 检查约束条件
if(strategy == null) {
stargety = fallbackStrategy();
}
return strategy.performCalculation();
}
提示: 除了使用IoC容器注入映射关系,我们还可以将对应应用程序的映射关系放到数据库或者其他外部配置文件,甚至注解中。通过IoC容器一次注入多个策略实例,可能需要占用多一些的系统资源。对于资源紧要的应用来说,可以考虑通过反射等方式按需构建具体策略实例,这个就留给你来完成吧!
在系统中合理的使用Strategy模式可以使得系统向着“高内聚,低耦合”的理想方向迈进,在改善应用程序代码结构的同时,进一步的提高产品质量。实际上,Strategy模式更是多态(Polymorphism)的完美体现。当你的OO内功修炼到“炉火纯青”的时候,也就发现,所谓的Strategy模式的概念,在你的脑海中或许已经淡然了。