截取自:https://mp.weixin.qq.com/s/HEyPBwbQZVRbFL1ivruVbg
無處不在的 if else 牛皮癬
無論你的編程啟蒙語言是什么罐寨,最早學(xué)會的邏輯控制語句一定是 if else,但是不幸的是它在你開始真正的編程工作以后赖晶,會變成一個損害項目質(zhì)量的壞習(xí)慣膳沽。
幾乎所有的項目都存在 if else 泛濫的問題窒盐,但是卻沒有引起足夠重視警惕亏镰,甚至被很多程序員認(rèn)為是正常現(xiàn)象套么。
首先我來解釋一下為什么 if else 這個看上去人畜無害的東西是有害的培己、是需要嚴(yán)格管控的:
- if else if ...else 以及類似的 switch 控制語句,本質(zhì)上是一種 hard coding 硬編碼行為胚泌,如果你同意“magic number 魔法數(shù)字”是一種錯誤的編程習(xí)慣省咨,那么同理,if else 也是錯誤的 hard coding 編程風(fēng)格玷室;
- hard coding 的問題在于當(dāng)需求發(fā)生改變時零蓉,需要到處去修改,很容易遺漏和出錯穷缤;
- 以一段代碼為例來具體分析:
if ("3".equals(object.getString("type"))){
String data = object.getString("data");
Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");
Map param = new HashMap();
param.put("phoneNumber", object.getString("mobile"));
List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);
if (list1 !=null){
for (Dperson dperson:list1){
dperson.setZmScore(zmf);
personBaseDaoI.saveOrUpdate(dperson);
AppFlowUtil.updateAppUserInfo(dperson.getToken(),null,null,zmf);
}
}
}
-
if ("3".equals(object.getString("type")))
- 顯然這里的"3"是一個 magic number敌蜂,沒人知道 3 是什么含義,只能推測津肛;
- 但是僅僅將“3”重構(gòu)成常量 ABC_XYZ 并不會改善多少章喉,因為 if (ABC_XYZ.equals(object.getString("type"))) 仍然是面向過程的編程風(fēng)格,無法擴(kuò)展身坐;
- 到處被引用的常量 ABC_XYZ 并沒有比到處被 hard coding 的 magic number 好多少秸脱,只不過有了含義而已;
- 把常量升級成 Enum 枚舉類型呢部蛇,也沒有好多少摊唇,當(dāng)需要判斷的類型增加了或判斷的規(guī)則改變了,還是需要到處修改——Shotgun Surgery(霰彈式修改)
-
并非所有的 if else 都有害涯鲁,比如上面示例中的 if (list1 !=null) { 就是無害的巷查,沒有必要去消除,也沒有消除它的可行性抹腿。判斷是否有害的依據(jù):
- 如果 if 判斷的變量狀態(tài)只有兩種可能性(比如 boolean吮便、比如 null 判斷)時,是無傷大雅的幢踏;
- 反之,如果 if 判斷的變量存在多種狀態(tài)许师,而且將來可能會增加新的狀態(tài)房蝉,那么這就是個問題;
- switch 判斷語句無疑是有害的微渠,因為使用 switch 的地方往往存在很多種狀態(tài)搭幻。
充血枚舉類型——Rich Enum Type
正如前面分析呈現(xiàn)的那樣,對于代碼中廣泛存在的狀態(tài)逞盆、類型 if 條件判斷檀蹋,僅僅把被比較的值重構(gòu)成常量或 enum 枚舉類型并沒有太大改善——使用者仍然直接依賴具體的枚舉值或常量,而不是依賴一個抽象云芦。
于是解決方案就自然浮出水面了:在 enum 枚舉類型基礎(chǔ)上進(jìn)一步抽象封裝俯逾,得到一個所謂的“充血”的枚舉類型贸桶,代碼說話:
- 實現(xiàn)多種系統(tǒng)通知機(jī)制,傳統(tǒng)做法:
enum NOTIFY_TYPE { email,sms,wechat; } //先定義一個enum——一個只定義了值不包含任何行為的“貧血”的枚舉類型
if(type==NOTIFY_TYPE.email){ //if判斷類型 調(diào)用不同通知機(jī)制的實現(xiàn)
桌肴。皇筛。。
}else if (type=NOTIFY_TYPE.sms){
坠七。水醋。。
}else{
彪置。拄踪。。
}
- 實現(xiàn)多種系統(tǒng)通知方式拳魁,充血枚舉類型——Rich Enum Type 模式:
enum NOTIFY_TYPE { //1惶桐、定義一個包含通知實現(xiàn)機(jī)制的“充血”的枚舉類型
email("郵件",NotifyMechanismInterface.byEmail()),
sms("短信",NotifyMechanismInterface.bySms()),
wechat("微信",NotifyMechanismInterface.byWechat());
String memo;
NotifyMechanismInterface notifyMechanism;
private NOTIFY_TYPE(String memo,NotifyMechanismInterface notifyMechanism){//2、私有構(gòu)造函數(shù)的猛,用于初始化枚舉值
this.memo=memo;
this.notifyMechanism=notifyMechanism;
}
//getters ...
}
public interface NotifyMechanismInterface{ //3耀盗、定義通知機(jī)制的接口或抽象父類
public boolean doNotify(String msg);
public static NotifyMechanismInterface byEmail(){//3.1 返回一個定義了郵件通知機(jī)制的策的實現(xiàn)——一個匿名內(nèi)部類實例
return new NotifyMechanismInterface(){
public boolean doNotify(String msg){
.......
}
};
}
public static NotifyMechanismInterface bySms(){//3.2 定義短信通知機(jī)制的實現(xiàn)策略
return new NotifyMechanismInterface(){
public boolean doNotify(String msg){
.......
}
};
}
public static NotifyMechanismInterface byWechat(){//3.3 定義微信通知機(jī)制的實現(xiàn)策略
return new NotifyMechanismInterface(){
public boolean doNotify(String msg){
.......
}
};
}
}
//4、使用場景
NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg);
-
充血枚舉類型——Rich Enum Type 模式的優(yōu)勢:
- 不難發(fā)現(xiàn)卦尊,這其實就是 enum 枚舉類型和 Strategy Pattern 策略模式的巧妙結(jié)合運用叛拷;
- 當(dāng)需要增加新的通知方式時,只需在枚舉類 NOTIFY_TYPE 增加一個值岂却,同時在策略接口 NotifyMechanismInterface 中增加一個 by 方法返回對應(yīng)的策略實現(xiàn)忿薇;
- 當(dāng)需要修改某個通知機(jī)制的實現(xiàn)細(xì)節(jié),只需修改 NotifyMechanismInterface 中對應(yīng)的策略實現(xiàn)躏哩;
- 無論新增還是修改通知機(jī)制署浩,調(diào)用方完全不受影響,仍然是NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg);
-
與傳統(tǒng) Strategy Pattern 策略模式的比較優(yōu)勢:常見的策略模式也能消滅 if else 判斷扫尺,但是實現(xiàn)起來比較麻煩筋栋,需要開發(fā)更多的 class 和代碼量:
- 每個策略實現(xiàn)需單獨定義成一個 class;
- 還需要一個 Context 類來做初始化——用 Map 把類型與對應(yīng)的策略實現(xiàn)做映射正驻;
- 使用時從 Context 獲取具體的策略弊攘;
-
Rich Enum Type 的進(jìn)一步的充血:
- 上面的例子中的枚舉類型包含了行為,因此已經(jīng)算作充血模型了姑曙,但是還可以為其進(jìn)一步充血襟交;
- 例如有些場景下,只是要對枚舉值做個簡單的計算獲得某種 flag 標(biāo)記伤靠,那就沒必要把計算邏輯抽象成 NotifyMechanismInterface 那樣的接口捣域,殺雞用了牛刀;
- 這時就可以在枚舉類型中增加 static function 封裝簡單的計算邏輯;
-
策略實現(xiàn)的進(jìn)一步抽象:
- 當(dāng)各個策略實現(xiàn)(byEmail bySms byWechat)存在共性部分焕梅、重復(fù)邏輯時迹鹅,可以將其抽取成一個抽象父類;
- 然后就像前一章節(jié)——業(yè)務(wù)模板 Pattern of NestedBusinessTemplate 那樣丘侠,在各個子類之間實現(xiàn)優(yōu)雅的邏輯分離和復(fù)用徒欣。