接手了個專案,被if..else搞懵逼了

程式設計師老貓發表於2023-12-24

背景

領導:“這個專案,今後就給你維護了啊,仔細點。”
小貓:“好,沒問題”。
可當滿懷信心的小貓開啟專案工程包翻看一些程式碼之後,瞬間懵逼沒了信心。
蒙圈
是這樣的
code2
還是這樣的
code1
平級的if else密密麻麻就算了,但是深套五六層的if else甚至七八層的真的是讓人摸不著北。

開啟最佳化

那麼就上面小貓遇到的這種情況,面對著幾代程式設計師精心堆積的屎山,試問閣下該如何應對?不慌,老貓羅列了以下解決方案,如果各位還有比較好的最佳化方法也歡迎留言。
optimize

我們對著上述目錄從簡單的開始介紹吧:

一、提前return法

當我們遇到空物件或者有部分滿足條件之後才能執行的時候,不要只想著正向邏輯,其實可以逆向思維,把不滿足條件的優先排除掉。這樣可以有效避免if else的深巢狀。
最佳化前程式碼:

if(condition){
//doSomething
}else{
}
return;

最佳化後如下:

if(!condition){
  return;
}

二、能省則省,規避最後的else

原來的程式碼:

public Result addUser() {
	if (StrUtil.equals(userStatus, "online")) {
	    return doStep1();
	} else {
		return doStep2();
	}
	// else 後面沒有其他業務時,可省略最後的else,使程式碼簡潔
}

最佳化後的程式碼:

public Result addUser() {
	if (StrUtil.equals(userStatus, "online")) {
      return doStep1();
	}
  return doStep2();
}

當然這裡面要注意的點是,一定要確認是最後的else,並沒有其他的業務邏輯。

三、 三目運算子

還是基於上面的程式碼,如果只有兩種業務的話,其實在一個方法裡面直接用三目運演算法進行執行即可。如下改造:

public Result addUser() {
	 return StrUtil.equals(userStatus, "online")) ?doStep1() : doStep2();
}

一個方法一行程式碼搞定。

四、使用optional

很多業務場景下,其實我們寫if 是為了判空,自從java8之後其實多了一個Optional神器,Optional 是個容器,它可以儲存型別 T 的值,或者僅僅儲存null。Optional 提供了很多方法,這樣我們就不用顯式進行空值檢測。Optional 類的引入很好的解決空指標異常。我們看下下面的最佳化方式:
程式碼最佳化前:

if (user == null) {
    throw new Exception("未查詢到使用者資訊");
}

if (user != null) {
    update(user); // 執行方法呼叫
}

程式碼最佳化後:

Optional.ofNullable(user).orElseThrow(() -> new Exception("未查詢到使用者資訊"));

Optional.ofNullable(user).ifPresent(user -> update(user));

隱式呼叫相當優雅。

五、設計模式最佳化法

設計模式最佳化法其實也是針對不同的場景使用不同的設計模式從而簡化多餘的if else。

第一種,合理使用責任鏈模式。

我們再具體結合一種場景,比方說現在頁面上有新註冊的使用者,他需要提交相關的身份資訊進行認證,此時,我們底層往往會對他提交的資訊做相關的校驗處理。
底層我們的校驗方式(1)需要驗證基本字非空性 (2)需要驗證身份資訊基礎欄位合法性 (2)需要呼叫第三方進行要素認證。
原始程式碼如下:

public void addUser(User user) {
	// 1.非空校驗
	if (StrUtil.isBlank(user.getUsername())) {
		throw new RuntimeException("使用者名稱為空!");
	}
	if (StrUtil.isBlank(user.getPassword())) {
		throw new RuntimeException("密碼為空!");
	}
	...
	
	// 2.格式校驗
	if (!ValidUtil.isIdCardNo(user.getIdCardNo())) {
		throw new RuntimeException("身份證號格式錯誤!");
	}
	if (!ValidUtil.isEmail(user.getEmail())) {
		throw new RuntimeException("手機號格式錯誤!");
	}
	if (!ValidUtil.isEmail(user.getEmail())) {
 		throw new RuntimeException("郵箱格式錯誤!");
	}
   	...

	// 3.要四素認證校驗
  if(!doFourStampVerify(User user)){
 		throw new RuntimeException("四要素認證失敗!");
  }
}

此處可能還有很多其他的省略的場景。所以單個檔案中的If else可能比想象中多的多。那麼我們如何用責任鏈模式進行最佳化呢?
改造程式碼如下,首先定義一個處理器介面:

/**
 * 處理器連結口
 */
public interface UserChainHandler {
    void handler(User user);
}

剩下不同的場景校驗只要去實現這個介面就可以了,不過需要定義好順序

@Component
@Order(1) // 指定注入順序
public class UserParamNullValidChainHandler implements UserChainHandler {
    @Override
    public void handler(User user) {
	   	// 1.非空校驗
	    if (StrUtil.isBlank(user.getUsername())) {
			throw new RuntimeException("使用者名稱為空!");
		}
		if (StrUtil.isBlank(user.getPassword())) {
			throw new RuntimeException("密碼為空!");
		}
}


@Component
@Order(1) // 指定注入順序
public class UserParamNullValidChainHandler implements UserChainHandler {
    @Override
    public void handler(User user) {
	   	// 1.非空校驗
	    if (StrUtil.isBlank(user.getUsername())) {
			throw new RuntimeException("使用者名稱為空!");
		}
    ...
}
/**
 * 格式校驗處理器
 */
@Component
@Order(2) // 指定注入順序
public class UserParamFormatValidChainHandler implements UserChainHandler {
 
    @Override
    public void handler(User user) {
	    // 2.格式校驗
		if (!ValidUtil.isIdCardNo(user.getIdCardNo())) {
			throw new RuntimeException("身份證號格式錯誤!");
		}
    ...
}

/**
 * 四要素處理器
 */
@Component
@Order(3) // 指定注入順序
public class FourElementVerifyChainHandler implements UserChainHandler {
 
    @Override
    public void handler(User user) {
	    // 2.格式校驗
		if (!doFourStampVerify(User user)) {
			throw new RuntimeException("四要素認證失敗!");
		}
}
//進行組裝
@Component
@RequiredArgsConstructor
public class UserChainContext {
    
    private final List<UserChainHandler> userChainHandlerList; // 自動注入責任鏈處理器
    
    /**
     * 責任鏈元件執行
     *
     * @param requestParam 請求引數
     */
    public void handler(User user) {
        // 此處根據 Ordered 實際值進行排序處理
        userChainHandlerList.forEach(x -> x.handler(user));
    }
}

最終我們們的原來的add方法進行這樣呼叫就好了

public void addUser(User user) {
	// 執行責任鏈
	userChainContext.handler(user);
}

第二種,合理使用策略模式+工廠模式。

假設我們遇到這樣一個場景,我們目前底層是一個會員系統,目前系統需要計算各種會員套餐的價格,然後套餐的具體模式主要是由上層系統傳遞指定給我們。如果只關注業務直接擼程式碼的話,應該是如下。

public Result calcPrice(CalcPriceParam calcPriceParam){
  //判斷對應的計算價格的場景
  Integer type = judgeType(calcPriceParam);
  //根據場景呼叫不同的方法 ,建議更好的編碼習慣是把type改成列舉型別哈~
  if(type == 1){
    return calcPriceForTypeOne();
  }
  if(type == 2){
    return calcPriceForTypeTwo();
  }
  if(type == 3){
    return calcPriceForTypeThree();
  }
  .....
  if(typr == 10){
    return calcPriceForTypeTen();
  }
}

顯而易見隨著會員價格場景套餐越來越多,我們的if也會越來越多。
但是如果使用策略模式的話,我們可以做到如下:

public interface Strategy {
  Result calcPrice(CalcPriceParam calcPriceParam);

  int getBizType();
}
@Service
public Class firstStragy implement Strategy {
  Result calcPrice(CalcPriceParam calcPriceParam) {
    ....
    return result;
  }

  int getBizType() {
    return 1;
  }
}
public Class secondStragy implement Strategy {
  Result calcPrice(CalcPriceParam calcPriceParam) {
    ....
    return result;
  }

  int getBizType() {
    return 2;
  }
}
@Service
public class StrategyContext{
  Map<Integer,CalcPriceInterface> strategyContextMap = new HashMap<>();
  //注入對應的策略類
  @Autowired
  Strategy[] strategys;
    
  @PostConstruct
  public void setStrategyContextMap(){
    for(Stragegy stragegy:strategys){
        strategyContextMap.put(stragegy.getCode,stragegy);
    }
  }

  //根據場景呼叫不同的方法 
  public Result calcPrice(CalcPriceParam calcPriceParam){
  	Integer type = judgeType(calcPriceParam);
    CalcPriceInterface calcPriceInstance = strategyContextMap.get(type);
    return calcPriceInstance.calcPrice(calcPriceParam);
  }
}

這樣一來,我們們上面的第一個方法中的If else的實現將會變得很簡單,如下:

@Autowired
StrategyContext strategyContext;

public Result calcPrice(CalcPriceParam calcPriceParam){
		strategyContext.calcPrice(calcPriceParam);
}

這樣即使新增新的計算模式,我們只需去實現Strategy介面並且重寫裡面兩個方法即可完成後續業務的擴充。程式碼優雅簡單,可維護性強。
以上就是用設計模式針對大量if else進行改造。

六、表驅動法

這種方式個人覺得有點像策略模式,但是又不需要單獨抽出相關類去承載註冊方法,而是簡單地將方法透過函式式的方式放到Map中,等到需要使用的時候再進行呼叫。
原始爛程式碼,我們還是參考上述會員費用金額計算的場景。我們可以進行如下方式最佳化:

Map<String, Function<?> action> actionMap = new HashMap<>();
action.put("type1",() -> {calcPriceForTypeOne()});
action.put("type2",() -> {calcPriceForTypeTwo()});
action.put("type3",() -> {calcPriceForTypeThree()});
...

// 使用
actionMap.get(action).apply();

當然如果想要再最佳化得好一些的話,可以進行介面抽取,然後進行實現,在此不展開,留下給小夥伴們思考一下。

七、其他場景靈活運用,幹掉if else

我們再回到之前小貓遇到的那兩個程式碼截圖,其實我們可以看到有個大量if else並排的程式碼其實主要是想要比較相關的屬性有沒有發生變化,如果發生變化,那麼則返回false,沒有變化則返回true。其實我們想想是不是可以透過重寫LogisticDO這個物件的equals方法來進行實現呢?這樣是不是也規避了大量的if else。

還有其他一些當然也是根據具體場景來解決,比方說,我需要根據不同的type型別,進行獲取不同的描述資訊,那麼此時我們是不是可以使用enum去維護呢?
如下:

if(status.equals(1)){
   return "訂單未支付";
}else if(status.equals(2)){
   return "訂單已支付"
}else if(status.equals(3)){
   return "訂單已發貨"
}
.....

最佳化後

@Getter
@AllArgsConstructor
public enum OrderStatusEnum {
    UN_PAID("1","訂單未支付"),
    PAIDED("2","訂單已支付"),
    SENDED("3","訂單已發貨"),
    .....;

    private String status;

    private String statusDes;

    static OrderStatusEnum of(String status) {
        for (OrderStatusEnum statusEnum : OrderStatusEnum.values()) {
            if (statusEnum.getStatus().equals(status)) {
                return statusEnum;
            }
        }
        return null;
    }
}

String orderStatusDes = OrderStatusEnum.of(orderStatus).getStatusDes();

等等還有其他一些,由於這些最佳化個人認為是沒法標準化的最佳化原則,不同的業務場景都不同,所以在此,老貓不將其放在通用最佳化中,認為這個是其他最佳化方式。

結束語

之前在某個技術論壇上看到大家在爭論這麼一個問題“如何避免將維護的專案發展成屎山?”大家發言踴躍。有說前期做好設計,有人說程式碼質量需要高一些,合理場景套用一些設計模式等等。
不過老貓認為專案無法避免發展成屎山,只是快慢而已,我也認為專案無法避免發展成“屎山”。其原因有三點,

  1. 專案程式碼維護者經過好幾輪,每次開發技術水平參差不齊,程式碼風格也不同。
  2. 專案迭代中途有很多突發狀況,比方說為了解決Hotfix臨時上線,為了趕專案臨時上線,大家為了趕工完成業務需求,程式碼質量可能就可想而知了。
  3. 雖然經過好幾輪研發之手,有的研發害怕改出業務問題,所以選擇繼續堆屎山。

說了這麼多,其實老貓最終想表達的是,雖然專案會最終淪為屎山,但是作為一個有追求的研發,我們就應當從每個小的if else著手,至少讓當前這個專案在你維護期間,讓其發展成屎山的速度變慢一些,或者能替之前的老前輩還掉一些技術債才是最好的,各位小夥伴你們覺得呢?

相關文章