一、需求開發修改代碼
一次需求開發時碰到如下所示方法代碼:
private OrderShoudSettlementAmount getOrderShoudSettlementAmount(OrderDTO orderMain, List<SettlementDetail> details) {
OrderShoudSettlementAmount settlementAmount = new OrderShoudSettlementAmount();
// 應結金額=33021-33002-32003+32001-31001
// 貨款佣金=33005+33002+32003+31001
long feeMoney33021 = 0;
long feeMoney33002 = 0;
long feeMoney32003 = 0;
long feeMoney32001 = 0;
long feeMoney31001 = 0;
long feeMoney33005 = 0;
for (SettlementDetail settlementDetail : details) {
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_FREIGHT_ZS_NOSETTLE.getVal())) {
feeMoney33021 += settlementDetail.getOassMoney();
}
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_FREIGHT_YJ_ZS_NOSETTLE.getVal())) {
feeMoney33002 += settlementDetail.getOassMoney();
}
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_GOODS_YJ_WJTZ.getVal())) {
feeMoney32003 += settlementDetail.getOassMoney();
}
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_GOODS_NOSETTLE.getVal())) {
feeMoney32001 += settlementDetail.getOassMoney();
}
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_BDYJ_YJ_NOSETTLE.getVal())) {
feeMoney31001 += settlementDetail.getOassMoney();
}
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_YFYJ_ZS_XSG.getVal())) {
feeMoney33005 += settlementDetail.getOassMoney();
}
}
long settlementMoney = feeMoney33021 - feeMoney33002 - feeMoney32003 + feeMoney32001 - feeMoney31001;
long goodCommissionMoney = feeMoney33005 + feeMoney33002 + feeMoney32003 + feeMoney31001;
settlementAmount.setSettlementAmount(settlementMoney);
settlementAmount.setGoodsCommission(goodCommissionMoney);
settlementAmount.setOrderId(orderMain.getOrderId());
settlementAmount.setOrgCode(orderMain.getOrgCode());
settlementAmount.setStationNo(String.valueOf(orderMain.getDeliveryStationNo()));
settlementAmount.setBillTime(new Date());
settlementAmount.setRetSuccess(false);
return settlementAmount;
}
該方法邏輯比較簡單,就是組裝OrderShoudSettlementAmount對象。其中需要計算2個金額,分別是settlementMoney和goodCommissionMoney。
本次需求新增了費項,需要修改該方法。代碼修改後如下所示:
private OrderShoudSettlementAmount getOrderShoudSettlementAmount(OrderDTO orderMain, List<SettlementDetail> details) {
OrderShoudSettlementAmount settlementAmount = new OrderShoudSettlementAmount();
// 應結金額=33021-33002-32003+32001-31001+34012-34013
// 貨款佣金=33005+33002+32003+31001+34013
long feeMoney33021 = 0;
long feeMoney33002 = 0;
long feeMoney32003 = 0;
long feeMoney32001 = 0;
long feeMoney31001 = 0;
long feeMoney33005 = 0;
// 本次需求新增費項
long feeMoney34012 = 0;
// 本次需求新增費項
long feeMoney34013 = 0;
for (SettlementDetail settlementDetail : details) {
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_FREIGHT_ZS_NOSETTLE.getVal())) {
feeMoney33021 += settlementDetail.getOassMoney();
}
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_FREIGHT_YJ_ZS_NOSETTLE.getVal())) {
feeMoney33002 += settlementDetail.getOassMoney();
}
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_GOODS_YJ_WJTZ.getVal())) {
feeMoney32003 += settlementDetail.getOassMoney();
}
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_GOODS_NOSETTLE.getVal())) {
feeMoney32001 += settlementDetail.getOassMoney();
}
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_BDYJ_YJ_NOSETTLE.getVal())) {
feeMoney31001 += settlementDetail.getOassMoney();
}
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_YFYJ_ZS_XSG.getVal())) {
feeMoney33005 += settlementDetail.getOassMoney();
}
// 本次需求新增費項
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_CHF_XSG_NOSETTLE.getVal())) {
feeMoney34012 += settlementDetail.getOassMoney();
}
// 本次需求新增費項
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_CHF_YJ_XSG.getVal())) {
feeMoney34013 += settlementDetail.getOassMoney();
}
}
// 本次需求新增費項追加計算 + feeMoney34012 - feeMoney34013
long settlementMoney = feeMoney33021 - feeMoney33002 - feeMoney32003 + feeMoney32001 - feeMoney31001 + feeMoney34012 - feeMoney34013;
// 本次需求新增費項追加計算 + feeMoney34013
long goodCommissionMoney = feeMoney33005 + feeMoney33002 + feeMoney32003 + feeMoney31001 + feeMoney34013;
settlementAmount.setSettlementAmount(settlementMoney);
settlementAmount.setGoodsCommission(goodCommissionMoney);
settlementAmount.setOrderId(orderMain.getOrderId());
settlementAmount.setOrgCode(orderMain.getOrgCode());
settlementAmount.setStationNo(String.valueOf(orderMain.getDeliveryStationNo()));
settlementAmount.setBillTime(new Date());
settlementAmount.setRetSuccess(false);
return settlementAmount;
}
二、嗅出代碼的壞味道
Martin Fowler在《重構:改善既有代碼的設計》一書中列出了22種代碼的壞味道:
1.Duplicated Code(重複的代碼) 2.Long Method(過長函數) 3.Large Class(過大類) 4.Long Parameter List(過長參數列) 5.Divergent Change(發散式變化) 6.Shotgun Surgery(霰彈式修改) 7.Feature Envy(依戀情結) 8.Data Clumps(數據泥團) 9.Primitive Obsession(基本型別偏執) 10.Switch Statements(switch驚悚現身) 11.Parallel Inheritance Hierarchies(平行繼承體系) 12.Lazy Class(冗贅類) 13.Speculative Generality(誇誇其談未來性) 14.Temporary Field(令人迷惑的暫時字段) 15.Message Chains(過度耦合的消息鏈) 16.Middle Man(中間人) 17.Inappropriate Intimacy(狎暱關係) 18.Alternative Classes with Different Interfaces(異曲同工的類) 19.Incomplete Library Class(不完美的程序庫類) 20.Data Class(純稚的數據類) 21.Refused Bequest(被拒絕的遺贈) 22.Comments(過多的註釋)
參照這22種代碼的壞味道,我在以上方法代碼中嗅出了2種代碼的壞味道:
壞味道1:Duplicated Code(重複的代碼)
for循環中對每種費項的累加操作是重複代碼,而且每次新增費項,還得不斷增加該重複操作。
for (SettlementDetail settlementDetail : details) {
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_FREIGHT_ZS_NOSETTLE.getVal())) {
feeMoney33021 += settlementDetail.getOassMoney();
}
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_FREIGHT_YJ_ZS_NOSETTLE.getVal())) {
feeMoney33002 += settlementDetail.getOassMoney();
}
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_GOODS_YJ_WJTZ.getVal())) {
feeMoney32003 += settlementDetail.getOassMoney();
}
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_GOODS_NOSETTLE.getVal())) {
feeMoney32001 += settlementDetail.getOassMoney();
}
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_BDYJ_YJ_NOSETTLE.getVal())) {
feeMoney31001 += settlementDetail.getOassMoney();
}
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_YFYJ_ZS_XSG.getVal())) {
feeMoney33005 += settlementDetail.getOassMoney();
}
// 本次需求新增費項
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_CHF_XSG_NOSETTLE.getVal())) {
feeMoney34012 += settlementDetail.getOassMoney();
}
// 本次需求新增費項
if (settlementDetail.getExpenseType().equals(FeeInfoEnum.FEE_INFO_CHF_YJ_XSG.getVal())) {
feeMoney34013 += settlementDetail.getOassMoney();
}
}
壞味道2:Divergent Change(發散式變化)
Martin Fowler在書中對該壞味道的部分解釋如下:
我們希望軟件能夠更容易被修改——畢竟軟件再怎麼説本來就該是“軟”的。一旦需要修改,我們希望能夠跳到系統的某一點,只在該處做修改。
現在該方法代碼因為新需求開發,修改多處。
其實,除了以上2種代碼的壞味道之外,該方法代碼最大的問題是面向過程式編碼而不是面向對象式的。
為什麼這麼説呢?
前面提到過該方法的主要作用是組裝OrderShoudSettlementAmount對象,那麼其邏輯就應該主要體現“組裝”,而不是計算金額。計算金額相關邏輯應該抽離到單獨的類中,這樣既符合面向對象編程思想,也能夠消除壞味道2。
三、重構代碼
針對前面嗅出的代碼壞味道,果斷進行重構。重構之後代碼如下所示:
private OrderShoudSettlementAmount getOrderShoudSettlementAmount(OrderDTO orderMain, List<SettlementDetail> details) {
OrderShoudSettlementAmount settlementAmount = new OrderShoudSettlementAmount();
Map<Integer, Long> expenseTypeToFeeMoneyMap = Maps.newHashMap();
for (SettlementDetail settlementDetail : details) {
long feeMoney = Optional.ofNullable(expenseTypeToFeeMoneyMap.get(settlementDetail.getExpenseType())).orElse(0L);
feeMoney += Optional.ofNullable(settlementDetail.getOassMoney()).orElse(0L);
expenseTypeToFeeMoneyMap.put(settlementDetail.getExpenseType(), feeMoney);
}
long settlementMoney = SettlementMoneyCalcFeeInfoEnum.calcSettlementMoney(expenseTypeToFeeMoneyMap);
long goodCommissionMoney = GoodCommissionMoneyCalcFeeInfoEnum.calcGoodCommissionMoney(expenseTypeToFeeMoneyMap);
settlementAmount.setSettlementAmount(settlementMoney);
settlementAmount.setGoodsCommission(goodCommissionMoney);
settlementAmount.setOrderId(orderMain.getOrderId());
settlementAmount.setOrgCode(orderMain.getOrgCode());
settlementAmount.setStationNo(String.valueOf(orderMain.getDeliveryStationNo()));
settlementAmount.setBillTime(new Date());
settlementAmount.setRetSuccess(false);
return settlementAmount;
}
enum SettlementMoneyCalcFeeInfoEnum {
/**計算項*/
FEE_33021(FeeInfoEnum.FEE_INFO_FREIGHT_ZS_NOSETTLE, "+"),
FEE_33002(FeeInfoEnum.FEE_INFO_FREIGHT_YJ_ZS_NOSETTLE, "-"),
FEE_32003(FeeInfoEnum.FEE_INFO_GOODS_YJ_WJTZ, "-"),
FEE_32001(FeeInfoEnum.FEE_INFO_GOODS_NOSETTLE, "+"),
FEE_31001(FeeInfoEnum.FEE_INFO_BDYJ_YJ_NOSETTLE, "-"),
FEE_34012(FeeInfoEnum.FEE_INFO_CHF_XSG_NOSETTLE, "+"),
FEE_34013(FeeInfoEnum.FEE_INFO_CHF_YJ_XSG, "-");
private final FeeInfoEnum feeInfoEnum;
private final String symbol;
SettlementMoneyCalcFeeInfoEnum(FeeInfoEnum feeInfoEnum, String symbol) {
this.feeInfoEnum = feeInfoEnum;
this.symbol = symbol;
}
public static long calcSettlementMoney(Map<Integer, Long> expenseTypeToFeeMoneyMap) {
// 應結金額=33021-33002-32003+32001-31001+34012-34013
long settlementMoney = 0L;
for (SettlementMoneyCalcFeeInfoEnum calcFeeInfoEnum : SettlementMoneyCalcFeeInfoEnum.values()) {
if ("+".equals(calcFeeInfoEnum.symbol)) {
settlementMoney += Optional
.ofNullable(expenseTypeToFeeMoneyMap.get(calcFeeInfoEnum.feeInfoEnum.getVal()))
.orElse(0L);
}
if ("-".equals(calcFeeInfoEnum.symbol)) {
settlementMoney -= Optional
.ofNullable(expenseTypeToFeeMoneyMap.get(calcFeeInfoEnum.feeInfoEnum.getVal()))
.orElse(0L);
}
}
return settlementMoney;
}
}
enum GoodCommissionMoneyCalcFeeInfoEnum {
/**計算項*/
FEE_33005(FeeInfoEnum.FEE_INFO_YFYJ_ZS_XSG),
FEE_33002(FeeInfoEnum.FEE_INFO_FREIGHT_YJ_ZS_NOSETTLE),
FEE_32003(FeeInfoEnum.FEE_INFO_GOODS_YJ_WJTZ),
FEE_31001(FeeInfoEnum.FEE_INFO_BDYJ_YJ_NOSETTLE),
FEE_34013(FeeInfoEnum.FEE_INFO_CHF_YJ_XSG);
private final FeeInfoEnum feeInfoEnum;
GoodCommissionMoneyCalcFeeInfoEnum(FeeInfoEnum feeInfoEnum) {
this.feeInfoEnum = feeInfoEnum;
}
public static long calcGoodCommissionMoney(Map<Integer, Long> expenseTypeToFeeMoneyMap) {
// 貨款佣金=33005+33002+32003+31001+34013
long goodCommissionMoney = 0L;
for (GoodCommissionMoneyCalcFeeInfoEnum calcFeeInfoEnum : GoodCommissionMoneyCalcFeeInfoEnum.values()) {
goodCommissionMoney += Optional
.ofNullable(expenseTypeToFeeMoneyMap.get(calcFeeInfoEnum.feeInfoEnum.getVal()))
.orElse(0L);
}
return goodCommissionMoney;
}
}
四、總結
以上重構的方法代碼比較簡單,有些人可能會覺得不重構也挺好的,代碼可讀性也不差,每次修改也就肉眼可見的幾個地方,沒必要在這上面花費時間。
如果你有以上想法,不妨瞭解下軟件工程中的“破窗效應”:
破窗效應指的是在軟件開發過程中,如果存在低質量的代碼或設計,如果不及時修復,就會導致其他開發人員也採用同樣的低質量方案。這會逐漸升級到更嚴重的問題,導致軟件系統變得難以維護、擴展和改進。因此,在軟件開發中,及時解決問題和保持代碼質量非常重要,以避免破窗效應對於整個項目造成的負面影響。
同時看看Martin Fowler在《重構:改善既有代碼的設計》一書中對重構的部分解釋:
重構的每個步驟都很簡單,甚至顯得有些過於簡單:你只需要把某個字段從一個類移到另一個類,把某些代碼從一個函數拉出來構成另一個函數,或是在繼承體系中把某些代碼推上推下就行了。但是,聚沙成塔,這些小小的修改累積起來就可以根本改善設計質量。
重構不僅能夠提高代碼質量,讓代碼優雅起來,同時也能讓我們學以致用。我們所學的設計思想、原則、模式等理論知識,往往在重構中能夠真正實踐。
作者:京東零售 加文雄
來源:京東雲開發者社區