有個項目里有好幾套產品規則,需要根據每個產品計算利息分賬和生成還款計划。項目里原先的代碼嘗試去封裝這個變化,每個產品規則創建了一個對應的類。為了方便理解,這里畫了一個類圖:
將代碼設計成這樣是一個好的趨勢,符合面向對象的思想,既能復用公共的邏輯,又更好去維護。但是,有一處代碼我覺得可以優化下,具體代碼大致如下:
// 其他代碼
...
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由具體的子類實現。
經過這樣修改,原本混亂的代碼邏輯,一下子清晰多了,並且維護起來也方便。