Java 更優雅地寫出 switch conditions

·

2 min read

Background

在追求簡潔的路上, 大量複雜的判斷可能會採用 switch 的方式判斷, 而為了限制狀態, 可能會搭配 enums 配合, 讓 switch conditions 更容易理解與維護.

enum Player {
  // 戰士
  WARRIOR,

  // 弓箭手
  ARCHER,

  // 法師
  WIZARD,
}
String swordAttack() {
  return "劍術攻擊";
}

String shootAttack() {
  return "射箭攻擊";
}

String fireAttack() {
  return "火焰攻擊";
}

// 攻擊
String attack(Player player) {
  switch(player) {
    // 戰士攻擊
    case WARRIOR:
      return this.swordAttack();

    // 弓箭手攻擊
    case ARCHER:
      return this.shootAttack();

    // 法師攻擊
    case WIZARD:
      return this.fireAttack();

    // 其他職業拋出異常  
    default:
      throw new IllegalArgumentException("Invalid player: " + player);
  }
}

像這樣的 code, 其實有更優雅的 strategy pattern 可以套用, 但在效益與成本的取捨上又有點用牛刀殺雞的感覺.

用 supplier 重構

Supplier 是 java8+ 的 lambda 滿好用的特性之一, 顧名思義就是一種供應手段, 用來封裝無參數的方法, 使其成為一種 Supplier, 封裝到 Supplier 的 method, 並不會馬上被執行, 而是透過 get(), 在某些層面上也算是一種 lazy loading 的應用.

String attack(Player player) {
  switch(player) {
    // 戰士攻擊
    case WARRIOR:
      Supplier<String> warriorSupplier = () -> this.swordAttack();
      return warriorSupplier.get();

    // 弓箭手攻擊
    case ARCHER:
      Supplier<String> archerSupplier = () -> this.shootAttack();
      return archerSupplier.get();

    // 法師攻擊
    case WIZARD:
      Supplier<String> wizardSupplier = () -> this.fireAttack();
      return wizardSupplier.get();

    // 其他職業拋出異常    
    default:
      throw new IllegalArgumentException("Invalid player: " + player);
  }
}

用 Map 重構

因為 Enums 讓狀態變成有限性, 所以 Map 的資料結構是一個更明確的作法, key, value 就直接對應成 Enums, Supplier.

String attack(Player player) {
  Map<Player, Supplier<String>> attackMap = new HashMap();
  attackMap.put(Player.WARRIOR, () -> this.swordAttack());
  attackMap.put(Player.ARCHER, () -> this.shootAttack());
  attackMap.put(Player.WIZARD, () -> this.fireAttack());

用 Stream 重構

用 stream 在開發體驗上可以以 stream 流的方式做更多邏輯擴充與處理與優化, 比如 filter(), sort() 之類的.

String attack(Player player) {
  Map<Player, Supplier<String>> attackMap = new HashMap();
  attackMap.put(Player.WARRIOR, () -> this.swordAttack());
  attackMap.put(Player.ARCHER, () -> this.shootAttack());
  attackMap.put(Player.WIZARD, () -> this.fireAttack());

  // 確保線程安全, 推薦使用 Immutable 類型
  final Map<Player, Supplier<Void>> map = Collections.unmodifiableMap(attackMap);

  // 藉由 Stream 方式串接整個流程
  map.entrySet().stream()
    .filter(entry -> player.equals(entry.getKey()))
    .map(Entry::getValue)
    .map(Supplier::get)
    .findFirst()
    .orElseThrow(() -> new IllegalArgumentException("Invalid type: " + type));  
}

當然上述的案例很明確的就是依據 player 的狀態來執行 attack, 直接 get Map 處理就好了.

String attack(Player player) {
  Map<Player, Supplier<String>> attackMap = new HashMap();
  attackMap.put(Player.WARRIOR, () -> this.swordAttack());
  attackMap.put(Player.ARCHER, () -> this.shootAttack());
  attackMap.put(Player.WIZARD, () -> this.fireAttack());

  // 確保線程安全, 推薦使用 Immutable 類型
  final Map<Player, Supplier<String>> map = Collections.unmodifiableMap(attackMap);

  return Optional.ofNullable(map.get(player))
    .orElseThrow(() -> new IllegalArgumentException("Invalid type: " + player));
}