常見重構技巧 - 5種方式去除多余的if else


常見重構技巧 - 去除多余的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接口

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
      • 操作符
      • 操作數
    • 規則引擎
  • 定義規則

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, 每天更新中...


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM