(譯)在Java如何取代巢狀的If判斷式

Kai
11 min readMay 1, 2019

--

原文連結(Original link)

1. 概述:

對於任何一種程式語言,選擇型的結構是非常重要的一部分,使用了大量巢狀的if判斷式,最終會使我們的程式碼變得更加艱深並難以維護。

在這篇教學中,我們來談談一些取代這些巢狀的if判斷式的方法,探索不同的做法來簡化我們的程式碼。

2. 實際範例:

我們常常會遇到牽扯各種條件的業務邏輯,而且每一種條件下的執行都不同。為了演示,我們設計一個 計算機(Calculator) 來實現,這個類定義一個函式,這個函式分別使用兩個變數與一個運算子作為輸入,並回傳兩個變數透過運算子計算的結果,像是這樣:

public class Calculator {
public int calculate(int a, int b, String operator) {
int result = Integer.MIN_VALUE;

if ("add".equals(operator)) {
result = a + b;
} else if ("multiply".equals(operator)) {
result = a * b;
} else if ("divide".equals(operator)) {
result = a / b;
} else if ("subtract".equals(operator)) {
result = a - b;
}
return result;
}
}

當然也可以用switch的方式來寫:

public int calculateUsingSwitch(int a, int b, String operator) {
switch (operator) {
case "add":
result = a + b;
break;
// ...其他的case
}
return result;
}

在我們經典的開發過程中,if判斷式會隨著需求自然的增長並且變得更加複雜,當然switch判斷式也會難以應付變得更加複雜的條件。

另一個副作用是巢狀的條件結構會讓邏輯變得難以管控,以這個例子來說,當我們需要一個新的運算子時,我們必須加入新的if判斷式並實作這個運算子的處理流程。

3. 重構:

我們來看看有哪些方法可以簡化並將上面的例子改善成易於管理的程式碼。

3.1 工廠類(Factory Class)

許多時候我們遇到需要在不同條件分支下執行類似的事情,這時我們就有機會可以使用工廠方法,根據不同的實作類型的行為獲得不同的結果。

舉例來說,我們可以定義一個Operation的介面,宣告了一個apply的函式:

public interface Operation {
int apply(int a, int b);
}

這個函式有兩個整數輸入,並且回傳一個整數結果,接著我們定義一個類(Addition)來實現加法:

public class Addition implements Operation {
@Override
public int apply(int a, int b) {
return a + b;
}
}

這時就可以製作一個工廠(OperatorFactory),根據傳入的運算子字串來回傳實作的物件:

public class OperatorFactory {
static Map<String, Operation> operationMap = new HashMap<>();
static {
operationMap.put("add", new Addition());
operationMap.put("divide", new Division());
// ...其餘的運算子
}

public static Optional<Operation> getOperation(String operator){
return Optional.ofNullable(operationMap.get(operator));
}
}

現在在 計算機 中,可以從工廠中查詢並獲得相對應的運算方式,然後運算出結果:

public int calculateUsingFactory(int a, int b, String operator) {
Operation targetOperation = OperatorFactory
.getOperation(operator)
.orElseThrow(
() -> new IllegalArgumentException("Invalid Operator"));
return targetOperation.apply(a, b);
}

在這個範例,我們可以看到工廠類如何將流程委派給低耦合的實作類。實際上這還是有機會只是把巢狀的if判斷式轉成在工廠中加入而已,因此這個範例中提供了一個Map(OperatorFactory#operationMap)來取代if的判斷式,我們可以維護這個Map來快速的取得實作的類,當然也可以在運行中初始化這個Map。

3.2 使用列舉(Enums)

除了使用Map,也可以用列舉來列出個別的業務邏輯,我們當然可以在if或是switch判斷式中使用列舉,或是在工廠模式中運用列舉來指派不同的業務邏輯。

也可以透過每個獨立的列舉(意思就是列舉中的每一個值)來實作。

來看看怎麼做吧,首先,我們定義一個列舉Operator:

public enum Operator {
ADD, MULTIPLY, SUBTRACT, DIVIDE
}

可以看出,列舉中每一個值代表著不同的運算子,我們會在接下來的運算使用到。我們總是在if或是switch判斷式使用列舉不同的值代表不同條件,但是現在我們要設計一個不同的方式來將處理邏輯交給Enum自己。

我們定義一個抽象函式讓列舉中每一個值來實現運算,例如:

public enum Operator {
ADD {
@Override
public int apply(int a, int b) {
return a + b;
}

},
MULTIPLY {
...
},

SUBTRACT {
...
},

DIVIDE {
...
}
;

public abstract int apply(int a, int b);
}

然後在 計算機 中,定義一個函式來運算:

public int calculate(int a, int b, Operator operator) {
return operator.apply(a, b);
}

現在,我們可以運用Enum的valueOf()轉換字串來呼叫運算:

@Test
public void whenCalculateUsingEnumOperator_thenReturnCorrectResult() {
Calculator calculator = new Calculator();
int result = calculator.calculate(3,4, Operator.valueOf("ADD"));
assertEquals(7, result);
}

3.3 命令模式(Command Pattern)

在前面的論述中,我們看過了用工廠類透過變數取得正確的實作類(物件),接下來,實作類(物件)會被用在 計算機 的運算。

我們可以設計一個函式 Calculator#calculate 來接受一個命令,並且執行這個命令,這將是另外一種取代巢狀if判斷式的方法。

首先我們定義一個介面Command:

public interface Command {
Integer execute();
}

接著用AddCommand實作:

public class AddCommand implements Command {
private int a;
private int b;
public AddCommand(int a, int b) {
this.a = a;
this.b = b;
}

@Override
public Integer execute() {
return a + b;
}
}

最後,在 計算機 中新增一個傳入Command介面的函式:

public int calculate(Command command) {
return command.execute();
}

接著,我們可以透過實例化AddCommand,並將實例傳給 Calculator#calculate來呼叫運算

@Test
public void whenCalculateUsingCommand_thenReturnCorrectResult() {
Calculator calculator = new Calculator();
int result = calculator.calculate(new AddCommand(3, 7));
assertEquals(10, result);
}

3.4 規則引擎(Rule Engine)

當我們最終寫了一大堆巢狀if判斷式,每一個條件都是一個業務規則,每個規則是為了後面正確的處理流程。規則引擎將這些複雜的程式從主流程上移除。RuleEngine評估規則(Rule)並依據輸入回傳結果。

讓我們進入接下來的範例。設計一個簡單的RuleEngine根據一系列的規則處理傳入的描述(Expression),並回傳對應規則的結果。首先,定義一個介面Rule:

public interface Rule {
boolean evaluate(Expression expression);
Result getResult();
}

再來,用一個RuleEngine實作:

public class RuleEngine {
private static List<Rule> rules = new ArrayList<>();

static {
rules.add(new AddRule());
}

public Result process(Expression expression) {
Rule rule = rules
.stream()
.filter(r -> r.evaluate(expression))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Expression does not matches any Rule"));
return rule.getResult();
}
}

可以看出RuleEngine接受Expression物件並回傳結果。現在設計Expression類當作資料的載體,包含兩個整數與一個運算子:

public class Expression {
private Integer x;
private Integer y;
private Operator operator;
}

最後我們定義一個客製的AddRule,它只有在Operator.ADD時才會動作:

public class AddRule implements Rule {
@Override
public boolean evaluate(Expression expression) {
boolean evalResult = false;
if (expression.getOperator() == Operator.ADD) {
this.result = expression.getX() + expression.getY();
evalResult = true;
}
return evalResult;
}
}

現在我們可以用Expression來呼叫RuleEngine了:

@Test
public void whenNumbersGivenToRuleEngine_thenReturnCorrectResult() {
Expression expression = new Expression(5, 5, Operator.ADD);
RuleEngine engine = new RuleEngine();
Result result = engine.process(expression);

assertNotNull(result);
assertEquals(10, result.getValue());
}

4. 總結

在這篇教學中,我們探索了幾種不同的方式來簡化複雜的程式碼,我們也可以學著有效的運用設計模式(design patterns)來取代巢狀if判斷式。

像往常一樣,GitHub中有提供完整的程式碼

寫在最後:

這篇文章是經過同意後的翻譯文,一方面練習我的英文能力,一方面也跟其他大神有交流的機會,這篇文中列出的程式碼會與原文的程式碼稍有出入,目的是在於讓讀者能更一目了然原著想表達的概念。當然,有任何指教也請不吝指出。

最後謝謝Eugen允許我轉譯他網站中的文章,網站上也有Java各式各樣的教學文章,非常受用!

--

--

Kai
Kai

Written by Kai

強大來自於體認自己有多渺小

No responses yet