優化 if/else 的四種設計模式
在日常開發中,我們經常會遇到需要根據不同條件執行不同邏輯的場景,導致代碼中出現大量的if/else嵌套。這不僅降低了代碼的可讀性和可維護性,還會增加后續擴展的難度。
本文將介紹四種優雅的設計模式來優化這種"條件爆炸"問題:

1 策略模式
01 概念
首先我們來看下策略模式的定義。
策略模式(Strategy Pattern)是行為型設計模式之一,它定義了一系列算法,并將每個算法封裝起來,使它們可以相互替換。 策略模式使得算法可以獨立于使用它們的客戶端變化。
怎么理解策略模式 ?
軟件開發中常常遇到這種情況,實現某一個功能有多種算法或者策略,我們需要根據環境或者條件的不同選擇不同的算法或者策略來完成該功能。
這么一看,不就是 if...else...的邏輯 :

圖中的實現邏輯非常簡單,當我們是初學者時,這樣寫沒有問題,只要能正常運行即可。但隨著經驗的增長,這段代碼明顯違反了 OOP 的兩個基本原則:
- 單一職責原則:一個類或模塊只負責完成一個職責或功能。
- 開閉原則:軟件實體(模塊、類、方法等)應該“對擴展開放,對修改關閉”。
由于違反了這兩個原則,當 if-else 塊中的邏輯日益復雜時,代碼會變得越來越難以維護,極容易出現問題。
策略模式的設計思路是:
定義一些獨立的類來封裝不同的算法,每一個類封裝一個具體的算法。
每一個封裝算法的類我們都可以稱之為策略 (Strategy) ,為了保證這些策略的一致性,一般會用一個抽象的策略類來做算法的定義,而具體每種算法則對應于一個具體策略類。
這種設計思想里,包含三個角色:
- Context: 環境類
- Strategy: 抽象策略類
- ConcreteStrategy: 具體策略類

對應的時序圖:

接下來,我們用代碼簡單演示一下。
02 例子
步驟 1:創建接口
public interface Strategy {
public int doOperation(int num1, int num2);
}
Strategy 接口定義了一個 doOperation 方法,所有具體策略類將實現這個接口,以提供具體的操作。
步驟 2:創建實現相同接口的具體類
OperationAdd 是一個具體的策略類,實現了加法操作。
public class OperationAdd implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
OperationSubtract 是另一個具體的策略類,實現了減法操作。
public class OperationSubtract implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
OperationMultiply 是第三個具體的策略類,實現了乘法操作。
public class OperationMultiply implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}
步驟 3:創建上下文類
public class Context {
private Strategy strategy;
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2) {
return strategy.doOperation(num1, num2);
}
}
Context 類持有一個策略對象的引用,并允許客戶端設置其策略。它提供了一個 executeStrategy 方法,用于執行策略并返回結果。
步驟 4:使用上下文來看到策略改變時的行為變化
public static void main(String[] args) {
Context context = new Context();
context.setStrategy(new OperationAdd());
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
context.setStrategy(new OperationSubstract());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
context.setStrategy(new OperationMultiply());
System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
}
執行結果:

從上面的例子,我們簡單總結下策略模式的優缺點:
1、優點
- 策略模式提供了對“開閉原則”的完美支持,用戶可以在不修改原有系統的基礎上選擇算法或行為,也可以靈活地增加新的算法或行為。
- 策略模式提供了管理相關的算法族的辦法。
- 使用策略模式可以避免使用多重條件轉移語句。
2、缺點
- 客戶端必須知道所有的策略類,并自行決定使用哪一個策略類。
- 策略模式將造成產生很多策略類,可以通過使用享元模式在一定程度上減少對象的數量。
2 SPI 機制
01 概念
SPI 全稱為 Service Provider Interface,是一種服務發現機制。
SPI 的本質是將接口實現類的全限定名配置在文件中,并由服務加載器讀取配置文件,加載實現類。這樣可以在運行時,動態為接口替換實現類。正因此特性,我們可以很容易的通過 SPI 機制為我們的程序提供拓展功能。

02 Java SPI : JDBC Driver
在JDBC4.0 之前,我們開發有連接數據庫的時候,通常先加載數據庫相關的驅動,然后再進行獲取連接等的操作。
// STEP 1: Register JDBC driver
Class.forName("com.mysql.jdbc.Driver");
// STEP 2: Open a connection
String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
Connection conn = DriverManager.getConnection(url,username,password);
JDBC4.0之后使用了 Java 的 SPI 擴展機制,不再需要用 Class.forName("com.mysql.jdbc.Driver") 來加載驅動,直接就可以獲取 JDBC 連接。
接下來,我們來看看應用如何加載 MySQL JDBC 8.0.22 驅動:

首先 DriverManager類是驅動管理器,也是驅動加載的入口。
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
在 Java 中,static 塊用于靜態初始化,它在類被加載到 Java 虛擬機中時執行。
靜態塊會加載實例化驅動,接下來我們看看loadInitialDrivers 方法。

加載驅動代碼包含四個步驟:
-
系統變量中獲取有關驅動的定義。
-
使用 SPI 來獲取驅動的實現類(字符串的形式)。
-
遍歷使用 SPI 獲取到的具體實現,實例化各個實現類。
-
根據第一步獲取到的驅動列表來實例化具體實現類。
我們重點關注 SPI 的用法,首先看第二步,使用 SPI 來獲取驅動的實現類 , 對應的代碼是:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
這里沒有去 META-INF/services目錄下查找配置文件,也沒有加載具體實現類,做的事情就是封裝了我們的接口類型和類加載器,并初始化了一個迭代器。
接著看第三步,遍歷使用SPI獲取到的具體實現,實例化各個實現類,對應的代碼如下:
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//遍歷所有的驅動實現
while(driversIterator.hasNext()) {
driversIterator.next();
}
在遍歷的時候,首先調用driversIterator.hasNext()方法,這里會搜索 classpath 下以及 jar 包中所有的META-INF/services目錄下的java.sql.Driver文件,并找到文件中的實現類的名字,此時并沒有實例化具體的實現類。
然后是調用driversIterator.next()方法,此時就會根據驅動名字具體實例化各個實現類了,現在驅動就被找到并實例化了。
這里有一個小小的疑問: 那么如果發現了多個 driver 的話,要選擇哪一個具體的實現呢?
那就是 JDBC URL 了,driver 的實現有一個約定,如果 driver 根據 JDBC URL 判斷不是自己可以處理的連接就直接返回空,DriverManager 就是基于這個約定直到找到第一個不返回 null 的連接為止。

JDBC Driver 是 Java SPI 機制的一個非常經典的應用場景,包含三個核心步驟:
- 定義 SPI 文件 ,指定 Driver class 全限定名 ;
- 通過
DriverManager靜態類自動加載驅動 ; - 加載之后,當需要獲取連接時,還需要根據 ConnectionUrl 判斷使用哪一個加載完成的 Driver 。
因此, JDBC Driver 的 SPI 機制是需要多個步驟配合來完成的 ,同時基于 Java SPI 機制的缺陷,同樣也無法按需加載。
03 按需加載:Dubbo SPI 機制
基于 Java SPI 的缺陷無法支持按需加載接口實現類,Dubbo 并未使用 Java SPI,而是重新實現了一套功能更強的 SPI 機制。
Dubbo SPI 的相關邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,我們可以加載指定的實現類。
Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路徑下,配置內容如下:
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
與 Java SPI 實現類配置不同,Dubbo SPI 是通過鍵值對的方式進行配置,這樣我們可以按需加載指定的實現類。
另外,在測試 Dubbo SPI 時,需要在 Robot 接口上標注 @SPI 注解。
下面來演示 Dubbo SPI 的用法:
public class DubboSPITest {
@Test
public void sayHello() throws Exception {
ExtensionLoader<Robot> extensionLoader =
ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
optimusPrime.sayHello();
Robot bumblebee = extensionLoader.getExtension("bumblebee");
bumblebee.sayHello();
}
}
測試結果如下 :

另外,Dubbo SPI 除了支持按需加載接口實現類,還增加了 IOC 和 AOP 等特性 。
SPI 機制的優勢:
- 實現完全解耦
- 新增實現無需修改主代碼
- 支持運行時動態發現和加載
3 責任鏈模式
01 概念
責任鏈模式是一種行為設計模式, 允許你將請求沿著處理者鏈進行發送。 收到請求后, 每個處理者均可對請求進行處理, 或將其傳遞給鏈上的下個處理者。

更簡單的描述是:責任鏈是一個鏈表結構,這條鏈上有多個節點,它們有相同的接口,各自有各自的責任和實現。當有數據輸入時,第一個節點看自己能否處理問題,如果可以就進行處理,如果不能就數據交給下一個節點進行處理,以此類推直到最后一個責任節點。
責任鏈模式的使用場景是:
- 有多個對象可以處理同一個請求,具體由哪個對象處理是在運行時確定的。
- 例如,有 ABC 三個處理者可以處理一個請求,請求 a 需要 AC 兩個處理者處理,請求 b 需要 BC 兩個處理者處理,同時兩個處理者之間可能有前后依賴關系,這時候就可以使用責任鏈模式
- 在不確定請求處理者的時候,向同一類型的處理者中的一個發送請求
02 例子
筆者曾經通過責任鏈模式優化神州專車支付流程 。

支付流程是通過定時任務執行,每次都需要去判斷用戶的賬號余額、優惠券、企業賬號余額是否可以抵扣,假如滿足條件,則從對應的賬號資產里扣除對應的金額 。
因為原來的代碼存在大量的 if/else 邏輯,業務邏輯相互雜糅,維護成本很高,勇哥花了一周時間重構了該邏輯 , 以下是演示偽代碼:
1、定義抽象處理者(Handler)
/**
* 支付處理器接口
* 定義所有支付方式需要實現的方法
*/
public interface PaymentHandler {
/**
* 判斷該支付處理器是否適用于當前用戶
* @param userId 用戶ID
* @return 是否適用
*/
boolean canProcess(String userId);
/**
* 執行支付操作
* @param userId 用戶ID
* @param amount 支付金額
* @return 支付結果
*/
PaymentResult processPayment(String userId, double amount);
}
/**
* 支付結果類
* 封裝支付操作的結果信息
*/
class PaymentResult {
private boolean success; // 支付是否成功
private double paidAmount; // 實際支付金額
private String message; // 支付結果描述信息
public PaymentResult(boolean success, double paidAmount, String message) {
this.success = success;
this.paidAmount = paidAmount;
this.message = message;
}
// Getter方法
public boolean isSuccess() {
return success;
}
public double getPaidAmount() {
return paidAmount;
}
public String getMessage() {
return message;
}
}
2、定義具體處理者(Concrete Handler)
/**
* 企業賬戶支付處理器實現
*/
public class CorporatePaymentHandler implements PaymentHandler {
private static final double MAX_CORPORATE_PAYMENT = 5000; // 企業賬戶最大支付限額
@Override
public boolean canProcess(String userId) {
// 只有以"emp_"開頭的用戶ID才能使用企業賬戶支付
return userId.startsWith("emp_");
}
@Override
public PaymentResult processPayment(String userId, double amount) {
System.out.printf("嘗試使用企業賬戶支付: 用戶[%s], 金額[%.2f]%n", userId, amount);
if (amount <= MAX_CORPORATE_PAYMENT) {
// 金額在限額內,全額支付
return new PaymentResult(true, amount, "企業賬戶支付成功");
} else {
// 超過限額,只支付限額部分
return new PaymentResult(true, MAX_CORPORATE_PAYMENT,
String.format("企業賬戶部分支付成功(%.2f),還需支付: %.2f",
MAX_CORPORATE_PAYMENT, amount - MAX_CORPORATE_PAYMENT));
}
}
}
/**
* 個人賬戶支付處理器實現
*/
public class PersonalPaymentHandler implements PaymentHandler {
private static final double MAX_PERSONAL_PAYMENT = 1000; // 個人賬戶最大支付限額
@Override
public boolean canProcess(String userId) {
// 所有用戶都可以使用個人賬戶支付
return true;
}
@Override
public PaymentResult processPayment(String userId, double amount) {
System.out.printf("嘗試使用個人賬戶支付: 用戶[%s], 金額[%.2f]%n", userId, amount);
if (amount <= MAX_PERSONAL_PAYMENT) {
// 金額在限額內,全額支付
return new PaymentResult(true, amount, "個人賬戶支付成功");
} else {
// 超過限額,只支付限額部分
return new PaymentResult(true, MAX_PERSONAL_PAYMENT,
String.format("個人賬戶部分支付成功(%.2f),還需支付: %.2f",
MAX_PERSONAL_PAYMENT, amount - MAX_PERSONAL_PAYMENT));
}
}
}
3、定義組合支付上下文(核心數據結構是鏈表)
/**
* 組合支付上下文
* 負責管理支付處理器并執行組合支付
*/
public class CompositePaymentContext {
private final List<PaymentHandler> handlers = new ArrayList<>();
/**
* 添加支付處理器
* @param handler 支付處理器實例
*/
public void addHandler(PaymentHandler handler) {
handlers.add(handler);
}
/**
* 執行組合支付
* @param userId 用戶ID
* @param totalAmount 總支付金額
* @return 支付結果映射表,key為處理器類名,value為支付結果
*/
public Map<String, PaymentResult> executePayment(String userId, double totalAmount) {
// 使用LinkedHashMap保持支付嘗試的順序
Map<String, PaymentResult> paymentResults = new LinkedHashMap<>();
double remaining = totalAmount; // 剩余待支付金額
// 按處理器順序嘗試支付
for (PaymentHandler handler : handlers) {
// 檢查處理器是否適用且還有金額需要支付
if (handler.canProcess(userId) && remaining > 0) {
// 執行支付
PaymentResult result = handler.processPayment(userId, remaining);
// 記錄支付結果
paymentResults.put(handler.getClass().getSimpleName(), result);
if (result.isSuccess()) {
// 支付成功,減少剩余金額
remaining -= result.getPaidAmount();
if (remaining <= 0) {
break; // 金額已支付完成,退出循環
}
}
}
}
// 如果還有剩余金額未支付,記錄剩余金額
if (remaining > 0) {
paymentResults.put("Remaining", new PaymentResult(false, remaining,
String.format("支付未完成,剩余金額: %.2f", remaining)));
}
return paymentResults;
}
}
4、測試類
/**
* 組合支付系統演示類
*/
public class CompositePaymentSystem {
public static void main(String[] args) {
// 初始化支付上下文
CompositePaymentContext paymentContext = new CompositePaymentContext();
// 注冊支付處理器(順序決定了支付嘗試的優先級)
paymentContext.addHandler(new CorporatePaymentHandler()); // 先嘗試企業賬戶
paymentContext.addHandler(new PersonalPaymentHandler()); // 再嘗試個人賬戶
// 測試用例1:企業用戶,只需企業賬戶支付
System.out.println("\n=== 測試用例1 ===");
System.out.println("用戶ID: emp_789, 支付金額: 3000.00");
Map<String, PaymentResult> results1 = paymentContext.executePayment("emp_789", 3000);
results1.forEach((handlerName, result) -> {
System.out.printf("[%s] %s (支付金額: %.2f)%n",
handlerName, result.getMessage(), result.getPaidAmount());
});
// 測試用例2:企業用戶,需要組合支付
System.out.println("\n=== 測試用例2 ===");
System.out.println("用戶ID: emp_789, 支付金額: 6000.00");
Map<String, PaymentResult> results2 = paymentContext.executePayment("emp_789", 6000);
results2.forEach((handlerName, result) -> {
System.out.printf("[%s] %s (支付金額: %.2f)%n",
handlerName, result.getMessage(), result.getPaidAmount());
});
// 測試用例3:個人用戶,只需個人賬戶支付
System.out.println("\n=== 測試用例3 ===");
System.out.println("用戶ID: user123, 支付金額: 1500.00");
Map<String, PaymentResult> results3 = paymentContext.executePayment("user123", 1500);
results3.forEach((handlerName, result) -> {
System.out.printf("[%s] %s (支付金額: %.2f)%n",
handlerName, result.getMessage(), result.getPaidAmount());
});
}
}
4 規則引擎
01 概念
電商系統中,經常會有營銷活動,比如支付訂單時給與用戶一定程度的優惠,比如活動規則:滿 1000 減200 ,滿 500 減 100 。
這種情況一般都是通過 if-else 做分支判斷處理,還是非常簡單的。
但假如需要頻繁的修改活動規則,那么就需要頻繁的修改代碼,非常不容易維護(開發成本 + 上線成本)。
此時,引入規則引擎有順其自然了。規則引擎的優勢如下:
- 降低開發成本
- 業務人員獨立配置業務規則,開發人員無需理解,以往需要業務人員告訴開發人員,開發人員需要理解才能開發,并且還需要大量的測試來確定是否正確,而且開發結果還容易和提出的業務有偏差,種種都導致開發成本上升
- 增加業務的透明度,業務人員配置之后其它人業務人員也能看到
- 提高了規則改動的效率和上線的速度
通過規則引擎,我們只需要將業務人員配置的規則轉換成一個規則字符串,然后將該規則字符串保存進數據庫中,當使用該規則時,只傳遞該規則所需要的參數,便可以直接計算出結果。
02 例子
回到剛才營銷活動 ,使用 AviatorScript 規則引擎的流程如下:
1、創建運營腳本,將腳本存儲在數據庫 或者 配置中心。
if (amount>=1000){
return 200;
}elsif(amount>=500){
return 100;
}else{
return 0;
}
2、定義調用方法 。
public static BigDecimal getDiscount(BigDecimal amount, String rule) {
// 執行規則并計算最終價格
Map<String, Object> env = new HashMap<String, Object>();
env.put("amount", amount);
Expression expression = AviatorEvaluator.compile(
DigestUtils.md5Hex(rule.getBytes()),
rule,
true);
Object result = expression.execute(env);
if (result != null) {
return new BigDecimal(String.valueOf(result));
}
return null;
}
3、執行測試例子
// step1 : 此處可以從配置中心 或者數據庫中獲取
String rule = "if (amount>=1000){\n" +
" return 200;\n" +
"}elsif(amount>=500){\n" +
" return 100;\n" +
"}else{\n" +
" return 0;\n" +
"}\n";
// step 2: 通過 AviatorScript 執行
BigDecimal discount = getDiscount(new BigDecimal("600"), rule);
System.out.println("discount:" + discount);
// 執行結果是: discount:100
測試類地址:

5 總結
在實際開發中,我們經常會遇到需要根據不同條件執行不同邏輯的場景。
傳統的if-else嵌套方式雖然直觀,但隨著業務復雜度增加,會導致代碼臃腫、可讀性差、維護困難等問題。
本文總結了四種優雅的設計模式來解決這類"條件爆炸"問題。
01. 策略模式:靈活切換策略
核心思想:定義一系列算法,將每個算法封裝起來,并使它們可以相互替換。
適用場景:
- 系統中有多個相似的類,僅在行為上有差異
- 需要在運行時動態選擇算法
- 需要避免暴露復雜的條件判斷
優勢:
- 符合開閉原則,易于擴展新策略
- 消除大量條件語句
- 算法可以自由切換
示例:支付系統中不同支付方式的處理,如信用卡、支付寶、微信支付等。
02. SPI機制:服務發現的優雅實現
核心思想:通過配置文件動態發現和加載服務實現。
適用場景:
- 需要實現插件化架構
- 服務提供方需要獨立于服務調用方
- 需要運行時動態發現服務
優勢:
- 實現完全解耦
- 新增實現無需修改主代碼
- 支持熱插拔
示例:JDBC驅動加載、Dubbo的擴展點實現。
03. 責任鏈模式:處理流程的鏈式傳遞
核心思想:將請求的發送者和接收者解耦,使多個對象都有機會處理請求。
適用場景:
- 有多個對象可以處理同一請求
- 需要動態指定處理流程
- 處理順序很重要
優勢:
- 降低耦合度
- 靈活調整處理順序
- 新增處理器方便
示例:支付流程中的多賬戶組合支付、審批流程等。
04. 規則引擎:業務規則的動態管理
核心思想:將業務規則從代碼中分離,實現動態配置。
適用場景:
- 業務規則頻繁變化
- 需要非技術人員配置規則
- 規則復雜度高
優勢:
- 業務人員可自主配置
- 降低開發成本
- 提高規則透明度
示例:營銷活動規則、風控規則等。
05 設計原則指導
在實際選擇解決方案時,應遵循以下原則:
- 單一職責原則:每個類/模塊只負責一個功能
- 開閉原則:對擴展開放,對修改關閉
- 高內聚低耦合:模塊內部高度聚合,模塊間依賴最小化
- KISS原則:保持簡單直接,不過度設計
沒有放之四海而皆準的解決方案,需要根據具體業務場景選擇最合適的方式,而不是一味追求技術的新穎或復雜。
最終目標是寫出可維護、可擴展、高內聚低耦合的代碼。
浙公網安備 33010602011771號