有个项目里有好几套产品规则,需要根据每个产品计算利息分账和生成还款计划。项目里原先的代码尝试去封装这个变化,每个产品规则创建了一个对应的类。为了方便理解,这里画了一个类图:
将代码设计成这样是一个好的趋势,符合面向对象的思想,既能复用公共的逻辑,又更好去维护。但是,有一处代码我觉得可以优化下,具体代码大致如下:
// 其他代码
...
ParamService paramService;
if (ProductTypeEnum.Axx.getValue() == proType) {
paramService = this.axxParamService;
} else if (ProductTypeEnum.Bxx.getValue() == proType) {
paramService = this.bxxParamService;
} else if (ProductTypeEnum.Cxx.getValue() == proType) {
paramService = this.cxxParamService;
} else if (ProductTypeEnum.Dxx.getValue() == proType) {
paramService = this.dxxParamService;
} else if (ProductTypeEnum.Exx.getValue() == proType) {
paramService = this.exxParamService;
} else if (ProductTypeEnum.Fxx.getValue() == proType) {
paramService = this.fxxParamService;
} else {
logger.error("invalid proType:{}", proType);
throw new BusinessExcepton("invalid proType:" + proType);
}
// 其他代码
...
简单工厂模式
倒不是说这段代码写得有多么糟糕,而是项目中有好几处都这样去使用,让代码不仅重复,而且也让业务代码更难阅读,不易维护。我们应该要提取重复的代码,并编写能够表达语义的代码。这里,比较适合的一个套路就是“简单工厂模式”,通过“简单工厂模式”将new对象的代码从业务代码中抽离出去,保持代码的“开闭原则”。上面的代码不是直接new的,而是通过Spring注入,不过本质上是对象的创建工作,不应该与业务代码耦合在一起。重构的做法就是将上面创建对象实例的代码封装到一个工厂类,由工厂类负责创建对于的对象实例。
@Component
public class ParamServiceFactory {
// 成员变量
...
public ParamService createParamService(int proType) {
switch(proType) {
case ProductTypeEnum.Axx.getValue(): this.axxParamService;
case ProductTypeEnum.Bxx.getValue(): this.bxxParamService;
case ProductTypeEnum.Cxx.getValue(): this.cxxParamService;
case ProductTypeEnum.Dxx.getValue(): this.dxxParamService;
case ProductTypeEnum.Exx.getValue(): this.exxParamService;
case ProductTypeEnum.Fxx.getValue(): this.fxxParamService;
default:
logger.error("invalid proType:{}", proType);
throw new BusinessExcepton("invalid proType:" + proType);
}
}
}
在需要ParamService对象实例时,传入一个适当的proType参数给ParamServiceFactory的createParamService(int proType)方法就行了。如果需要增加产品规则,只需要改动ParamServiceFactory这个类,而不像之前那要需要改动好几个地方。并且,将原来业务代码中的近20行代码,缩减为1行,让我们更容易阅读业务代码。
模板方法的应用
前面是重构了创建对象的代码,将创建产品规则的类对象代码从业务代码中分离抽取,一方面降低了业务代码与产品规则代码的耦合度,方便之后调整产品规则;另一方面,降低了业务代码的复杂度,便于后期维护。不过,这个项目还有值得推敲的地方,原来的开发针对每个产品规则,创建了一个具体ParamService,这本来是个不错的想法,但是具体的方法实现十分混乱,让刚开始接触这段代码的我,阅读代码十分难受。
这里涉及的业务逻辑并不复杂,开头说过,就是计算利息分账和生成还款计划。生成还款计划没什么好说的,每个产品都有自己的一套还款计划规则,因此,父类ParamService生成还款计划的方法是一个抽象方法是没有问题的。但是,计算利息分账里面包含了计算免息券、判断是否批扣等逻辑,这些逻辑每个产品都是相同的。在实现代码时就应该将这些逻辑放到父类中去,以便子类继承并使用。如果公共逻辑都放在子类中,不仅让子类方法变得臃肿,而且也没有真正享受到继承的好处,反而只是徒增了几个实现类,没有丝毫的好处。
这里我们完全可以使用模板方法模式,把计算免息券、判断是否批扣等逻辑在父类实现,而计算利息分账的逻辑在子类实现。这样做最大程度地复用了代码,并且公共的逻辑只在一个地方,所以容易修改。
使用模板方法后的类图如下:
可以与文章开头未修改的类图比较下,发现父类ParamService多了几个方法,正是这几个方法把之前重复的代码给分离出去了。这里最关键的方法是getInterestSplit()方法,先看下代码:
public final double getInterestSplit(RepayRequest repayRequest) {
double reduce = getReduction(repayRequest);
boolean isBatch = isBatch(repayRequest);
return calculateInterestSplit(repayRequest, reduce, isBatch);
}
getInterestSplit()方法被定义为final,这是为了防止子类去修改这个方法。getReduction()和isBatch()在父类实现,calculateInterestSplit由具体的子类实现。
经过这样修改,原本混乱的代码逻辑,一下子清晰多了,并且维护起来也方便。