求求你們了,別再寫滿屏的 if/else 了!

為什么我們寫的代碼都是 if-else?
程序員想必都經(jīng)歷過這樣的場景:剛開始自己寫的代碼很簡潔,邏輯清晰,函數(shù)精簡,沒有一個(gè) if-else,可隨著代碼邏輯不斷完善和業(yè)務(wù)的瞬息萬變:比如需要對(duì)入?yún)⑦M(jìn)行類型和值進(jìn)行判斷;這里要判斷下對(duì)象是否為 null;不同類型執(zhí)行不同的流程。
落地到具體實(shí)現(xiàn)只能不停地加 if-else 來處理,漸漸地,代碼變得越來越龐大,函數(shù)越來越長,文件行數(shù)也迅速突破上千行,維護(hù)難度也越來越大,到后期基本達(dá)到一種難以維護(hù)的狀態(tài)。
雖然我們都很不情愿寫出滿屏 if-else 的代碼,可邏輯上就是需要特殊判斷,很絕望,可也沒辦法避免啊。
其實(shí)回頭看看自己的代碼,寫 if-else 不外乎兩種場景:異常邏輯處理和不同狀態(tài)處理。
兩者最主要的區(qū)別是:異常邏輯處理說明只能一個(gè)分支是正常流程,而不同狀態(tài)處理都所有分支都是正常流程。
怎么理解?舉個(gè)例子:
?1//舉例一:異常邏輯處理例子
?2Object?obj?=?getObj();
?3if?(obj?!=?null)?{
?4????//do?something
?5}else{
?6????//do?something
?7}
?8
?9//舉例二:狀態(tài)處理例子
10Object?obj?=?getObj();
11if?(obj.getType?==?1)?{
12????//do?something
13}else?if?(obj.getType?==?2)?{
14????//do?something
15}else{
16????//do?something
17}
第一個(gè)例子 if (obj != null) 是異常處理,是代碼健壯性判斷,只有 if 里面才是正常的處理流程,else 分支是出錯(cuò)處理流程;而第二個(gè)例子不管 type 等于 1,2 還是其他情況,都屬于業(yè)務(wù)的正常流程。對(duì)于這兩種情況重構(gòu)的方法也不一樣。
代碼 if-else 代碼太多有什么缺點(diǎn)?
缺點(diǎn)相當(dāng)明顯了:最大的問題是代碼邏輯復(fù)雜,維護(hù)性差,極容易引發(fā) bug。如果使用 if-else,說明 if 分支和 else 分支的重視是同等的,但大多數(shù)情況并非如此,容易引起誤解和理解困難。
是否有好的方法優(yōu)化?如何重構(gòu)?
方法肯定是有的。重構(gòu) if-else 時(shí),心中無時(shí)無刻把握一個(gè)原則:
盡可能地維持正常流程代碼在最外層。
意思是說,可以寫 if-else 語句時(shí)一定要盡量保持主干代碼是正常流程,避免嵌套過深。
實(shí)現(xiàn)的手段有:減少嵌套、移除臨時(shí)變量、條件取反判斷、合并條件表達(dá)式等。
下面舉幾個(gè)實(shí)例來講解這些重構(gòu)方法:
異常邏輯處理型重構(gòu)方法實(shí)例一
重構(gòu)前:
?1double?disablityAmount(){
?2????if(_seniority?2)
?3????????return?0;
?4
?5????if(_monthsDisabled?>?12)
?6????????return?0;
?7
?8????if(_isPartTime)
?9????????return?0;
10
11????//do?somethig
12}
重構(gòu)后:
1double?disablityAmount(){
2????if(_seniority?2?||?_monthsDisabled?>?12?||?_isPartTime)
3????????return?0;
4
5????//do?somethig
6}
這里的重構(gòu)手法叫合并條件表達(dá)式:如果有一系列條件測(cè)試都得到相同結(jié)果,將這些結(jié)果測(cè)試合并為一個(gè)條件表達(dá)式。推薦看下:狗屎一樣的代碼重構(gòu)。
這個(gè)重構(gòu)手法簡單易懂,帶來的效果也非常明顯,能有效地較少if語句,減少代碼量邏輯上也更加易懂。
異常邏輯處理型重構(gòu)方法實(shí)例二
重構(gòu)前:
?1double?getPayAmount(){
?2????double?result;
?3????if(_isDead)?{
?4????????result?=?deadAmount();
?5????}else{
?6????????if(_isSeparated){
?7????????????result?=?separatedAmount();
?8????????}
?9????????else{
10????????????if(_isRetired){
11????????????????result?=?retiredAmount();
12????????????else{
13????????????????result?=?normalPayAmount();
14????????????}
15????????}
16????}
17????return?result;
18}
重構(gòu)后:
?1double?getPayAmount(){
?2????if(_isDead)
?3????????return?deadAmount();
?4
?5????if(_isSeparated)
?6????????return?separatedAmount();
?7
?8????if(_isRetired)
?9????????return?retiredAmount();
10
11????return?normalPayAmount();
12}
怎么樣?比對(duì)兩個(gè)版本,會(huì)發(fā)現(xiàn)重構(gòu)后的版本邏輯清晰,簡潔易懂。
和重構(gòu)前到底有什么區(qū)別呢?
最大的區(qū)別是減少 if-else 嵌套。可以看到,最初的版本 if-else 最深的嵌套有三層,看上去邏輯分支非常多,進(jìn)到里面基本都要被繞暈。其實(shí),仔細(xì)想想嵌套內(nèi)的 if-else 和最外層并沒有關(guān)聯(lián)性的,完全可以提取最頂層。
改為平行關(guān)系,而非包含關(guān)系,if-else 數(shù)量沒有變化,但是邏輯清晰明了,一目了然。
另一個(gè)重構(gòu)點(diǎn)是廢除了 result 臨時(shí)變量,直接 return 返回。好處也顯而易見直接結(jié)束流程,縮短異常分支流程。原來的做法先賦值給 result 最后統(tǒng)一 return,那么對(duì)于最后 return 的值到底是那個(gè)函數(shù)返回的結(jié)果不明確,增加了一層理解難度。
總結(jié)重構(gòu)的要點(diǎn):如果 if-else 嵌套沒有關(guān)聯(lián)性,直接提取到第一層,一定要避免邏輯嵌套太深。盡量減少臨時(shí)變量改用 return 直接返回。
異常邏輯處理型重構(gòu)方法實(shí)例三
重構(gòu)前:
1public?double?getAdjustedCapital(){
2????double?result?=?0.0;
3????if(_capital?>?0.0?){
4????????if(_intRate?>?0?&&?_duration?>0){
5????????????resutl?=?(_income?/?_duration)?*ADJ_FACTOR;
6????????}
7????}
8????return?result;
9}
第一步,運(yùn)用第一招,減少嵌套和移除臨時(shí)變量:
1public?double?getAdjustedCapital(){
2????if(_capital?<=?0.0?){
3????????return?0.0;
4????}
5????if(_intRate?>?0?&&?_duration?>0){
6????????return?(_income?/?_duration)?*ADJ_FACTOR;
7????}
8????return?0.0;
9}
這樣重構(gòu)后,還不夠,因?yàn)橹饕恼Z句 (_income / _duration) *ADJ_FACTOR; 在 if 內(nèi)部,并非在最外層,根據(jù)優(yōu)化原則(盡可能地維持正常流程代碼在最外層),可以再繼續(xù)重構(gòu):
?1public?double?getAdjustedCapital(){
?2????if(_capital?<=?0.0?){
?3????????return?0.0;
?4????}
?5????if(_intRate?<=?0?||?_duration?<=?0){
?6????????return?0.0;
?7????}
?8
?9????return?(_income?/?_duration)?*ADJ_FACTOR;
10}
這才是好的代碼風(fēng)格,邏輯清晰,一目了然,沒有 if-else 嵌套難以理解的流程。
這里用到的重構(gòu)方法是:將條件反轉(zhuǎn)使異常情況先退出,讓正常流程維持在主干流程。Spring Boot 如何干掉 if else?推薦看下。
異常邏輯處理型重構(gòu)方法實(shí)例四
重構(gòu)前:
?1???/*?查找年齡大于18歲且為男性的學(xué)生列表?*/
?2????public?ArrayList?getStudents(int?uid){
?3????????ArrayList?result?=?new?ArrayList();
?4????????Student?stu?=?getStudentByUid(uid);
?5????????if?(stu?!=?null)?{
?6????????????Teacher?teacher?=?stu.getTeacher();
?7????????????if(teacher?!=?null){
?8????????????????ArrayList?students?=?teacher.getStudents();
?9????????????????if(students?!=?null){
10????????????????????for(Student?student?:?students){
11????????????????????????if(student.getAge()?>?=?18?&&?student.getGender()?==?MALE){
12????????????????????????????result.add(student);
13????????????????????????}
14????????????????????}
15????????????????}else?{
16????????????????????logger.error("獲取學(xué)生列表失敗");
17????????????????}
18????????????}else?{
19????????????????logger.error("獲取老師信息失敗");
20????????????}
21????????}?else?{
22????????????logger.error("獲取學(xué)生信息失敗");
23????????}
24????????return?result;
25????}
典型的"箭頭型"代碼,最大的問題是嵌套過深,解決方法是異常條件先退出,保持主干流程是核心流程:
重構(gòu)后:
?1???/*?查找年齡大于18歲且為男性的學(xué)生列表?*/
?2????public?ArrayList?getStudents(int?uid){
?3????????ArrayList?result?=?new?ArrayList();
?4????????Student?stu?=?getStudentByUid(uid);
?5????????if?(stu?==?null)?{
?6????????????logger.error("獲取學(xué)生信息失敗");
?7????????????return?result;
?8????????}
?9
10????????Teacher?teacher?=?stu.getTeacher();
11????????if(teacher?==?null){
12????????????logger.error("獲取老師信息失敗");
13????????????return?result;
14????????}
15
16????????ArrayList?students?=?teacher.getStudents();
17????????if(students?==?null){
18????????????logger.error("獲取學(xué)生列表失敗");
19????????????return?result;
20????????}
21
22????????for(Student?student?:?students){
23????????????if(student.getAge()?>?18?&&?student.getGender()?==?MALE){
24????????????????result.add(student);
25????????????}
26????????}
27????????return?result;
28????}
狀態(tài)處理型重構(gòu)方法實(shí)例一
重構(gòu)前:
?1double?getPayAmount(){
?2????Object?obj?=?getObj();
?3????double?money?=?0;
?4????if?(obj.getType?==?1)?{
?5????????ObjectA?objA?=?obj.getObjectA();
?6????????money?=?objA.getMoney()*obj.getNormalMoneryA();
?7????}
?8????else?if?(obj.getType?==?2)?{
?9????????ObjectB?objB?=?obj.getObjectB();
10????????money?=?objB.getMoney()*obj.getNormalMoneryB()+1000;
11????}
12}
重構(gòu)后:
?1double?getPayAmount(){
?2????Object?obj?=?getObj();
?3????if?(obj.getType?==?1)?{
?4????????return?getType1Money(obj);
?5????}
?6????else?if?(obj.getType?==?2)?{
?7????????return?getType2Money(obj);
?8????}
?9}
10
11double?getType1Money(Object?obj){
12????ObjectA?objA?=?obj.getObjectA();
13????return?objA.getMoney()*obj.getNormalMoneryA();
14}
15
16double?getType2Money(Object?obj){
17????ObjectB?objB?=?obj.getObjectB();
18????return?objB.getMoney()*obj.getNormalMoneryB()+1000;
19}
這里使用的重構(gòu)方法是:把 if-else 內(nèi)的代碼都封裝成一個(gè)公共函數(shù)。函數(shù)的好處是屏蔽內(nèi)部實(shí)現(xiàn),縮短 if-else 分支的代碼。代碼結(jié)構(gòu)和邏輯上清晰,能一下看出來每一個(gè)條件內(nèi)做的功能。
狀態(tài)處理型重構(gòu)方法實(shí)例二
針對(duì)狀態(tài)處理的代碼,一種優(yōu)雅的做法是用多態(tài)取代條件表達(dá)式(《重構(gòu)》推薦做法)。
你手上有個(gè)條件表達(dá)式,它根據(jù)對(duì)象類型的不同而選擇不同的行為。將這個(gè)表達(dá)式的每個(gè)分支放進(jìn)一個(gè)子類內(nèi)的覆寫函數(shù)中,然后將原始函數(shù)聲明為抽象函數(shù)。
重構(gòu)前:
?1double?getSpeed(){
?2????switch(_type){
?3????????case?EUROPEAN:
?4????????????return?getBaseSpeed();
?5????????case?AFRICAN:
?6????????????return?getBaseSpeed()-getLoadFactor()*_numberOfCoconuts;
?7????????case?NORWEGIAN_BLUE:
?8????????????return?(_isNailed)?0:getBaseSpeed(_voltage);
?9????}
10}
重構(gòu)后:
?1class?Bird{
?2????abstract?double?getSpeed();
?3}
?4
?5class?European?extends?Bird{
?6????double?getSpeed(){
?7????????return?getBaseSpeed();
?8????}
?9}
10
11class?African?extends?Bird{
12????double?getSpeed(){
13????????return?getBaseSpeed()-getLoadFactor()*_numberOfCoconuts;
14????}
15}
16
17class?NorwegianBlue?extends?Bird{
18????double?getSpeed(){
19????????return?(_isNailed)?0:getBaseSpeed(_voltage);
20????}
21}
可以看到,使用多態(tài)后直接沒有了 if-else,但使用多態(tài)對(duì)原來代碼修改過大,需要一番功夫才行。最好在設(shè)計(jì)之初就使用多態(tài)方式。
總結(jié)
if-else 代碼是每一個(gè)程序員最容易寫出的代碼,同時(shí)也是最容易被寫爛的代碼,稍不注意,就產(chǎn)生一堆難以維護(hù)和邏輯混亂的代碼。
針對(duì)條件型代碼重構(gòu)把握一個(gè)原則:
盡可能地維持正常流程代碼在最外層,保持主干流程是正常核心流程。
為維持這個(gè)原則:合并條件表達(dá)式可以有效地減少if語句數(shù)目;減少嵌套能減少深層次邏輯;異常條件先退出自然而然主干流程就是正常流程。
針對(duì)狀態(tài)處理型重構(gòu)方法有兩種:一種是把不同狀態(tài)的操作封裝成函數(shù),簡短 if-else 內(nèi)代碼行數(shù);另一種是利用面向?qū)ο蠖鄳B(tài)特性直接干掉了條件判斷。
現(xiàn)在回頭看看自己的代碼,犯了哪些典型錯(cuò)誤,趕緊運(yùn)用這些重構(gòu)方法重構(gòu)代碼吧!!
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 BY-SA 版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接和本聲明。
本文鏈接:https://blog.csdn.net/qq_35440678/article/details/77939999
公眾號(hào)「菜鳥翻身」,會(huì)推薦 GitHub 上有用的項(xiàng)目,一分鐘 get 一個(gè)優(yōu)秀的開源項(xiàng)目,挖掘開源的價(jià)值,墻裂推薦關(guān)注。
