0.問題概述
代碼可讀性是衡量代碼品質的重要标準,可讀性也是可維護性、可擴充性的保證,因為代碼是連接配接程式員和機器的中間橋梁,要對雙邊友好。Quora 上有一個文章: “What are some of the most basic things every programmer should know?”
其中:
- Code that’s hard to understand is hard to maintain.
- Code that’s hard to maintain is next to useless.
也強調了"easy understand"代碼的重要性。
寫這篇文章的契機是在研讀Apache ShenYu項目時,看到了很大一坨的if else語句,如下:
這裡并非評論這段代碼寫法有問題,因為我還并沒有深入到項目細節之中,可能這已經是多輪優化的結果嘞。
但是這個多層if else的形式引發了我的思考,因為我也曾在項目代碼中引入過如此繁重的if else結構,并在Code Review中被指出了問題。從那以後,我對if else的最大容忍層數就是三層。
我把大量if else的場景按照深度和廣度兩個次元劃分為兩種情況:
- 嵌套層級過深
- 平鋪範圍太廣
下面就讨論一下,當代碼中存在大量這樣結構的代碼的時候,該如何優化?
1.解決方案
1.1 盡早傳回
又稱衛語句,即Guard Statement
WikiPedia:
In computer programming, a guard is a boolean expression that must evaluate to true if the program execution is to continue in the branch in question.
Regardless of which programming language is used, a guard clause, guard code, or guard statement, is a check of integrity preconditions used to avoid errors during execution. A typical example is checking that a reference about to be processed is not null, which avoids null-pointer failures. Other uses include using a boolean field for idempotence (so subsequent calls are nops), as in the dispose pattern. The guard provides an early exit from a subroutine, and is a commonly used deviation from structured programming, removing one level of nesting and resulting in flatter code:[1] replacing if guard { ... } with if not guard: return; ....
實際應用:
if (CollectionUtils.isNotEmpty(list)) {
// do something
} else {
return xxx;
}
使用盡早傳回優化:
if (CollectionUtils.isEmpty(list)) {
return xxx;
}
// do something
可以看到,優化後的代碼不僅節省了一個else語句,也能讓後續的"do something"節省一層if else包裹,代碼看起來更幹淨一些
結合這個例子再說一下我對衛語句的了解:
可以将“衛”了解為“門衛”,門衛的作用是檢查過濾,隻有符合條件的語句,才可以繼續執行,否則直接勸返(return)。吐槽一下這種中文直譯有些晦澀,未免有點“德先生賽先生”的意思了。。。
1.2 使用switch或三元運算符
可以利用文法知識,對if else進行簡化,
例如,當if else滿足一定條件時:
if (condition1) {
doSomeThing1();
} else if (condition2) {
doSomeThing2();
} else if (condition3) {
doSomeThing3();
} else if (condition4) {
doSomeThing4();
} else {
doSomeThing5();
}...
可以使用switch case文法進行替換
或,
例如使用三元運算符進行指派操作:
Integer num = obejct == null ? 1 : object.value();
1.3 政策模式
1.3.1 概念
政策模式是一種行為設計模式,即一個對象有一個确定的行為,在不同場景下,這些行為有不同的算法實作。
例如從内蒙通過公共交通去北京是一個确定的行為,在天上這種場景可以選擇飛機,地上的場景可以選擇火車~
政策模式一般包含三個要素:
- 抽象政策(Abstract strategy):定義所謂的“确定的行為”,一般由接口或抽象類實作
- 具體實作(Concrete strategy):封裝對應場景下的具體算法實作。
- 上下文(Context):負責具體實作政策的管理并供對象使用。
1.3.2 使用場景
- 一個接口或抽象類的各個子類都是為了解決相同的問題,區分這些子類的隻有方法實作的不同。
- 代碼中使用大量if else或大面積switch case來選擇具體的子實作類
1.3.3 實際應用
例如:
if ("man".equals(strategy)) {
// Perform related operations
} else if ("woman".equals(strategy)) {
// Perform operations related to women
} else if ("other".equals(strategy)) {
// Perform other operations
}
上面一段代碼,每一個if分支完成的都是相同的操作,隻是在不同的性别場景下,操作方法的實作不同,那麼就可以使用政策模式進行優化:
首先,定義一個抽象政策接口:
public interface Strategy {
void run() throws Exception;
}
然後,進行不同政策的實作:
//Men's strategy implementation class
@Slf4j
public class ManStrategy implements Strategy {
@Override
public void run() throws Exception {
// Fast man's logic
log.debug("Execute the logic related to men...");
}
}
//Women's strategy implementation class
@Slf4j
public class WomanStrategy implements Strategy {
@Override
public void run() throws Exception {
// Fast woman's logic
log.debug("Execute women related logic...");
}
}
//Others' policy implementation class
@Slf4j
public class OtherStrategy implements Strategy {
@Override
public void run() throws Exception {
// Fast other logic
log.debug("Perform other related logic...");
}
}
最後,進行政策的應用:
public class StrategyTest {
public static void main(String[] args) {
try {
Strategy strategy = initMap("man");
strategy.run();
} catch (Exception e) {
e.printStackTrace();
}
}
//Initialize the Map to obtain a gender policy
private static Strategy initMap(String key) {
//Use simple example
HashMap<String, Strategy> map = new HashMap<>();
map.put("man", new ManStrategy());
map.put("woman", new WomanStrategy());
map.put("other", new OtherStrategy());
return map.get(key);
}
}
1.3.4 優劣勢分析及優化
1.3.4.1 劣勢
整體上來看,使用政策模式雖然剔除了大量的if else語句,但是也引入了更多的類檔案,同時在Context中需要維護一個類似系統資料庫的map對象,當增加政策實作時,容易忘記。
優化措施:
在Java中,可以使用函數式程式設計進行優化:
@Slf4j
public class StrategyTest {
public static void main(String[] args) {
//Use simple example
HashMap<String, Strategy> map = new HashMap<>();
map.put("man", () -> log.debug("Execute the logic related to men..."));
map.put("woman", () -> log.debug("Execute women related logic..."));
map.put("other", () -> log.debug("Execute logic related to others..."));
try {
map.get("woman").run();
} catch (Exception e) {
e.printStackTrace();
}
}
}
或者,使用枚舉進行優化:
@Slf4j
public enum Strategy {
//Man state
MAN(0) {
@Override
void run() {
//Perform related operations
log.debug("Execute the logic related to men");
}
},
//Woman state
WOMAN(1) {
@Override
void run() {
//Perform operations related to women
log.debug("Execute women related logic");
}
},
//Other status
OTHER(2) {
@Override
void run() {
//Perform other related operations
log.debug("Perform other related logic");
}
};
abstract void run();
public int statusCode;
Strategy(int statusCode) {
this.statusCode = statusCode;
}
}
public static void main(String[] args) {
try {
//Simple use example
String param = String.valueOf(Strategy.WOMAN);
Strategy strategy = Strategy.valueOf(param);
strategy.run();
} catch (Exception e) {
e.printStackTrace();
}
}
除此以外,在用戶端實際使用政策時,即對象進行方法的調用時,用戶端必須知道這個政策的所有實作子類,并需要了解這些子類之間的不同以及各自的應用場景,這樣用戶端才能選擇合适的政策實作“确定的行為”。
1.3.4.2 優勢
- 最直接的好處就是可以讓又臭又長的if else代碼塊看起來更幹淨。
- 面向對象的三大特點:封裝、繼承、多态,在政策模式中都能找到影子。面向接口程式設計,代碼的可擴充性好
- 代碼的可測性好,Mock更友善,減少了分支判斷,實作類隻需要各自測試即可。
1.4 Optional
if else分支判斷的很多情況都是進行非空條件的判斷,Optional是Java8開始提供的新特性,使用這個文法特性,也可以減少代碼中if else的數量,例如:
優化前:
String str = "Hello World!";
if (str != null) {
System.out.println(str);
} else {
System.out.println("Null");
}
優化後:
Optional<String> optional = Optional.of("Hello World!");
optional.ifPresentOrElse(System.out::println, () -> System.out.println("Null"));
1.5 系統資料庫
這種方式和政策模式有相似之處,但系統資料庫更自由,不需要提煉接口,隻需要将自定義實作在系統資料庫中注冊即可。
例如,優化前:
if (param.equals(value1)) {
doAction1(someParams);
}else if (param.equals(value2)) {
doAction2(someParams);
}else if (param.equals(value3)) {
doAction3(someParams);
}
優化後:
//Generic here? For the convenience of demonstration, it can be replaced with the real type we need in actual development
Map<?, Function<?> action> actionMappings = new HashMap<>();
// When init
actionMappings.put(value1, (someParams) -> { doAction1(someParams)});
actionMappings.put(value2, (someParams) -> { doAction2(someParams)});
actionMappings.put(value3, (someParams) -> { doAction3(someParams)});
// Omit null judgment
actionMappings.get(param).apply(someParams);
1.6 責任鍊模式
先來看一段代碼:
public void handle(request) {
if (handlerA.canHandle(request)) {
handlerA.handleRequest(request);
} else if (handlerB.canHandle(request)) {
handlerB.handleRequest(request);
} else if (handlerC.canHandle(request)) {
handlerC.handleRequest(request);
}
}
代碼中也是存在一坨if else語句,但是和上述例子不同之處在于,if條件判斷權在每個handler元件中,每一個handler的判斷方式也可能不盡相同,相當靈活,同一個request可能同時滿足多個if條件
解決方案就是參考開源元件中Filter或者Interceptor責任鍊機制,優化後代碼:
public void handle(request) {
handlerA.handleRequest(request);
}
public abstract class Handler {
protected Handler next;
public abstract void handleRequest(Request request);
public void setNext(Handler next) { this.next = next; }
}
public class HandlerA extends Handler {
public void handleRequest(Request request) {
if (canHandle(request)) doHandle(request);
else if (next != null) next.handleRequest(request);
}
}
2.總結&思考
這篇文章主要介紹了代碼中if else代碼塊泛濫時的治理措施,在實際應用時可根據具體場景選擇合理的方案。
其實代碼中存在大面積if else本無問題,用一句網絡流行語來反駁就是:“你就說能不能用吧!”。但是作為有追求的工程師,我們要對項目以及代碼負責,要及時的識别到代碼中的壞味道,并持續重構優化。最後還想說一定要擁抱開源,多研讀他人優秀代碼,并臨摹、思考、實踐,日拱一卒,不期而至。
3.參考
- https://programmer.ink/think/how-to-optimize-if-there-are-too-many-if-statements-in-java-code-of-series-17.html
- WikiPedia
- Quora
作者:京東零售 韓超
來源:京東雲開發者社群