[Java 入門] コンソールアプリのメニュー選択で、Switch文などを使わずに分岐させる

## 概要
Java入門で開発するコンソールアプリでは、メニューを表示し、標準入力で受け取った番号の操作を実行するものがあります。
```
[メニューを選択してください]
1 表示
2 追加
3 戻る
input > 1
```
みたいなやつです。
これをSwitch文で分岐させる、というのがよくある設計ですが、抽象メソッドと列挙子を活用して実装してみましょう。

## 実装
まずはメニューを記述する列挙子です。

```java:MemberMenu.java
public enum MemberMenu {
  SHOW("1", "表示") {
    @Override
    public void execute() {
      MemberList.getMember()
        .stream()
        .sorted(Comparator.comparing(Member::getId))
        .forEach(m -> System.out.println(m.show()));
    }
  },
  ADD("2", "追加") {
    @Override
    public void execute() {
      new AddMember().execute();
    }
  },
  // 中略
  ;

  // メニュー番号
  private final String menuNum;
  // メニュー名
  private final String label;
  
  /** コンストラクタ */
  private MemberMenu(String menuNum, String label) {
    this.menuNum = menuNum;
    this.label = label;
  }

  /** 各メニューでの動作を記述するメソッド */
  public abstract void execute();

  /** 存在するメニュー番号かどうか判定 */
  public boolean containsMenu(String target) {
    return Arrays
      .stream(values())
      .anyMatch(r -> r.getMenuNum().equals(target));
  }

  /** メニュー番号から逆引き */
  public static MemberMenu valueOf(String num) {
    return Arrays
      .stream(values())
      .filter(s -> s.getMenuNum.equals(num))
      .findFirst();
  }

  // [番号 名前]の形式にする 
  public String toString() {
    return menuNum + " " + label;
  }
  // 全メニューをメニュー番号順にソートして、改行で繋げる
  public String toMenu() {
    return Arrays
      .stream(values())
      .sorted(Comparator.comparing(s -> s.getMenuNum()))
      .map(s -> s.toString())
      .collect(Collectors.joining(System.lineSeparator()));
  }
}
```
抽象メソッド ``` execute()``` を使い、各メニューでの動作を記述しました。
続いてこれを呼び出す部分です。

```java:MemberView.java
public class MemberView {
  public void execute() {
    String menuNum;
    // 存在するメニュー番号が入力されるまで繰り返し
    do {
      System.out.println("[メニューを選択してください]");

      // メニュー表示
      System.out.println(MemberMenu.toMenu());

      // 標準入力から取得
      menuNum = ScannerManager.getInstance().getScanner().nextLine();

    } while(!containsMenu(menuNum));

    // 選ばれたメニューを実行
    MemberMenu.valueOf(menuNum).execute();
  }
}

```
このように記述すれば、呼び出し元で分岐せずとも、呼びたいメニューが実行できます。
他のクラスは適当に補間してください。

---
## 列挙子クラスの共通インターフェース利用
メニューが多数あるシステムの場合、同じ処理が重複するのは好ましくないかと思います。共通部分をインターフェースに記述する方法を記載しておきます。


```java:Ienum.java
/**
 * Enum用インターフェース
 */
public interface Ienum<E extends Enum<E>> {
	/** コードを取得 */
	public String getCode();
	/** ラベルを取得 */
	public String getLabel();

	/**
	 * Enumに変換
	 * @return
	 */
	@SuppressWarnings("unchecked")
	default E toEnum() {
		return (E) this;
	}

	/**
	 * コードが含まれているかを判定するメソッド
	 * @param code
	 * @return 判定結果
	 *             <ul>
	 *                 <li>含まれている場合 - true</li>
	 *                 <li>含まれていない場合 - false</li>
	 *             </ul>
	 */
	public static <E extends Enum<E>> boolean containsCode(Class<? extends Ienum<E>> enumClass, String code) {
		return Arrays
				.stream(enumClass.getEnumConstants())
				.anyMatch(s -> s.getCode().equals(code));
	}

	/**
	 * コードから逆引きするメソッド
	 * @param <E>       検索対象のenum
	 * @param enumClass 検索対象のenum
	 * @param code      検索するコード
	 * @return 逆引き結果
	 *             <ul>
	 *                 <li>存在する場合 - 逆引き結果</dd>
	 *                 <li>存在しない場合 - IllegalArgumentException</li>
	 *             </ul>
	 */
	public static <E extends Enum<E>> E getByCode(Class<? extends Ienum<E>> enumClass, String code) {
		return Arrays
				.stream(enumClass.getEnumConstants())
				.filter(s -> s.getCode().equals(code))
				.map(Ienum::toEnum)
				.findFirst()
				.orElseThrow(() -> new IllegalArgumentException());
	}

	/**
	 * 一覧生成
	 * @param <E>       対象のEnum
	 * @param enumClass 対象のEnum
	 * @return メニューを改行で繋いだ文字列
	 */
	public static <E extends Enum<E>> String toMenu(Class<? extends Ienum<E>> enumClass) {
		return Arrays
				.stream(enumClass.getEnumConstants())
				.sorted(Comparator.comparing(s -> s.getCode()))
				.map(s -> s.getCode() + " " + s.getLabel())
				.collect(Collectors.joining(System.lineSeparator()));
	}

}
```

上のインターフェースを継承させたメニューの列挙子は以下です。

```java:MemberMenu.java
public enum MemberMenu implements Ienum<MemberMenu> {
	SHOW("1", "表示") {
		@Override
		public void execute() {
            MemberList.getMember()
                .stream()
                .sorted(Comparator.comparing(Member::getId))
                .forEach(m -> System.out.println(m.show()));
    	}
	},
	;

	/** メニューコード */
	private String code;
	/** メニュー名 */
	private String label;

	public abstract void execute();

	/**
	 * コンストラクタ
	 * @param code  メニューコード
	 * @param label メニュー名
	 */
	MemberMenu(String code, String label) {
		this.code = code;
		this.label = label;
	}

	@Override
	public String getCode() {
		return code;
	}
	@Override
	public String getLabel() {
    	return label;
    }
}
```
最後にこれの利用方法です。

```java:MemberView.java
public class MemberView {
  public void execute() {
    String menuNum;
    // 存在するメニュー番号が入力されるまで繰り返し
    do {
      System.out.println("[メニューを選択してください]");
      // メニュー表示
      System.out.println(Ienum.toMenu(MemberMenu.class));
      // 標準入力から取得
      menuNum = ScannerManager.getInstance().getScanner().nextLine();

    } while(!Imenu.containsMenu(MemberMenu.class, menuNum));

    // 選ばれたメニューを実行
    Ienum.getByCode(MemberMenu.class, menuNum).execute();
  }
}
```

## まとめ
Enumでそれぞれに処理を記述することで、条件と処理をまとめることが出来ます。
これによって複雑な分岐を記述する手間も省け、読む人にも読みやすい、良いコードになると思います。


ミス等あれば修正します。

## 参考文献
[【Java】if文による条件分岐よりもEnumを使った"設定"を-Strategy Enum](https://qiita.com/te-te/items/d7b3092faf48a34fdd3c) (2019)