常見重構技巧 - 去除多余的if else
最為常見的是代碼中使用很多的if/else,或者switch/case;如何重構呢?方法特別多,本文帶你學習其中的技巧。
出現if/else和switch/case的場景
通常業務代碼會包含這樣的邏輯:每種條件下會有不同的處理邏輯。比如兩個數a和b之間可以通過不同的操作符(+,-,*,/)進行計算,初學者通常會這么寫:
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/case:
public int calculateUsingSwitch(int a, int b, String operator) {
switch (operator) {
case "add":
result = a + b;
break;
// other cases
}
return result;
}
這種最基礎的代碼如何重構呢?
重構思路
有非常多的重構方法來解決這個問題, 這里會列舉很多方法,在實際應用中可能會根據場景進行一些調整;另外不要糾結這些例子中顯而易見的缺陷(比如沒用常量,沒考慮多線程等等),而是把重心放在學習其中的思路上。@pdai
方式一 - 工廠類
- 定義一個操作接口
public interface Operation {
int apply(int a, int b);
}
- 實現操作, 這里只以add為例
public class Addition implements Operation {
@Override
public int apply(int a, int b) {
return a + b;
}
}
- 實現操作工廠
public class OperatorFactory {
static Map<String, Operation> operationMap = new HashMap<>();
static {
operationMap.put("add", new Addition());
operationMap.put("divide", new Division());
// more operators
}
public static Optional<Operation> getOperation(String operator) {
return Optional.ofNullable(operationMap.get(operator));
}
}
- 在Calculator中調用
public int calculateUsingFactory(int a, int b, String operator) {
Operation targetOperation = OperatorFactory
.getOperation(operator)
.orElseThrow(() -> new IllegalArgumentException("Invalid Operator"));
return targetOperation.apply(a, b);
}
對於上面為什么方法名是apply
,Optional
怎么用? 請參考這篇:
- Java 8 - 函數編程(lambda表達式)
- Lambda 表達式的特點?
- Lambda 表達式使用和Stream下的接口?
- 函數接口定義和使用,四大內置函數接口Consumer,Function,Supplier, Predicate.
- Comparator排序為例貫穿所有知識點。
- Java 8 - Optional類深度解析
- Optional類的意義?
- Optional類有哪些常用的方法?
- Optional舉例貫穿所有知識點
- 如何解決多重類嵌套Null值判斷?
方式二 - 枚舉
枚舉適合類型固定,可枚舉的情況,比如這的操作符; 同時枚舉中是可以提供方法實現的,這就是我們可以通過枚舉進行重構的原因。
- 定義操作符枚舉
public enum Operator {
ADD {
@Override
public int apply(int a, int b) {
return a + b;
}
},
// other operators
public abstract int apply(int a, int b);
}
- 在Calculator中調用
public int calculate(int a, int b, Operator operator) {
return operator.apply(a, b);
}
- 寫個測試用例測試下:
@Test
public void whenCalculateUsingEnumOperator_thenReturnCorrectResult() {
Calculator calculator = new Calculator();
int result = calculator.calculate(3, 4, Operator.valueOf("ADD"));
assertEquals(7, result);
}
看是否很簡單?
方法三 - 命令模式
命令模式也是非常常用的重構方式, 把每個操作符當作一個Command。
-
首先讓我們回顧下什么是命令模式
-
看這篇文章:行為型 - 命令模式(Command)
- 命令模式(Command pattern): 將"請求"封閉成對象, 以便使用不同的請求,隊列或者日志來參數化其他對象. 命令模式也支持可撤銷的操作。
- Command: 命令
- Receiver: 命令接收者,也就是命令真正的執行者
- Invoker: 通過它來調用命令
- Client: 可以設置命令與命令的接收者
- 命令模式(Command pattern): 將"請求"封閉成對象, 以便使用不同的請求,隊列或者日志來參數化其他對象. 命令模式也支持可撤銷的操作。
-
-
Command接口
public interface Command {
Integer execute();
}
- 實現Command
public class AddCommand implements Command {
// Instance variables
public AddCommand(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public Integer execute() {
return a + b;
}
}
- 在Calculator中調用
public int calculate(Command command) {
return command.execute();
}
- 測試用例
@Test
public void whenCalculateUsingCommand_thenReturnCorrectResult() {
Calculator calculator = new Calculator();
int result = calculator.calculate(new AddCommand(3, 7));
assertEquals(10, result);
}
注意,這里new AddCommand(3, 7)
仍然沒有解決動態獲取操作符問題,所以通常來說可以結合簡單工廠模式來調用:
- 創建型 - 簡單工廠(Simple Factory)
- 簡單工廠(Simple Factory),它把實例化的操作單獨放到一個類中,這個類就成為簡單工廠類,讓簡單工廠類來決定應該用哪個具體子類來實例化,這樣做能把客戶類和具體子類的實現解耦,客戶類不再需要知道有哪些子類以及應當實例化哪個子類
方法四 - 規則引擎
規則引擎適合規則很多且可能動態變化的情況,在先要搞清楚一點Java OOP,即類的抽象:
-
這里可以抽象出哪些類?// 頭腦中需要有這種自動轉化
- 規則Rule
- 規則接口
- 具體規則的泛化實現
- 表達式Expression
- 操作符
- 操作數
- 規則引擎
- 規則Rule
-
定義規則
public interface Rule {
boolean evaluate(Expression expression);
Result getResult();
}
- 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;
}
}
- 表達式
public class Expression {
private Integer x;
private Integer y;
private Operator operator;
}
- 規則引擎
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();
}
}
- 測試用例
@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());
}
方法五 - 策略模式
策略模式比命令模式更為常用,而且在實際業務邏輯開發中需要注入一定的(比如通過Spring的
@Autowired
來注入bean),這時通過策略模式可以巧妙的重構
-
什么是策略模式?
- 我們再復習下:行為型 - 策略(Strategy)
- 策略模式(strategy pattern): 定義了算法族, 分別封閉起來, 讓它們之間可以互相替換, 此模式讓算法的變化獨立於使用算法的客戶
- Strategy 接口定義了一個算法族,它們都具有 behavior() 方法。
- Context 是使用到該算法族的類,其中的 doSomething() 方法會調用 behavior(),setStrategy(in Strategy) 方法可以動態地改變 strategy 對象,也就是說能動態地改變 Context 所使用的算法。
-
Spring中需要注入資源重構?
如果是在實現業務邏輯需要注入框架中資源呢?比如通過Spring的
@Autowired
來注入bean。可以這樣實現:
- 操作 // 很巧妙
public interface Opt {
int apply(int a, int b);
}
@Component(value = "addOpt")
public class AddOpt implements Opt {
@Autowired
xxxAddResource resource; // 這里通過Spring框架注入了資源
@Override
public int apply(int a, int b) {
return resource.process(a, b);
}
}
@Component(value = "devideOpt")
public class devideOpt implements Opt {
@Autowired
xxxDivResource resource; // 這里通過Spring框架注入了資源
@Override
public int apply(int a, int b) {
return resource.process(a, b);
}
}
- 策略
@Component
public class OptStrategyContext{
private Map<String, Opt> strategyMap = new ConcurrentHashMap<>();
@Autowired
public OptStrategyContext(Map<String, TalkService> strategyMap) {
this.strategyMap.clear();
this.strategyMap.putAll(strategyMap);
}
public int apply(Sting opt, int a, int b) {
return strategyMap.get(opt).apply(a, b);
}
}
上述代碼在實現中非常常見。
一些反思
最怕的是剛學會成語,就什么地方都想用成語。
-
真的要這么重構嗎?
- 在實際開發中,切記
最怕的是剛學會成語,就什么地方都想用成語
; 很多時候不是考慮是否是最佳實現,而是折中(通常是業務和代價的折中,開發和維護的折中...),在適當的時候做適當的重構。 - 很多時候,讓團隊可持續性的維護代碼便是最佳;
- 重構后會生成很多類,一個簡單業務搞這么復雜?所以你需要權衡
- 在實際開發中,切記
參考文章
更多內容
最全的Java后端知識體系 https://www.pdai.tech, 每天更新中...。