LoginSignup
0
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-11-24

概要

Java入門で開発するコンソールアプリでは、メニューを表示し、標準入力で受け取った番号の操作を実行するものがあります。

[メニューを選択してください]
1 表示
2 追加
3 戻る
input > 1

みたいなやつです。
これをSwitch文で分岐させる、というのがよくある設計ですが、抽象メソッドと列挙子を活用して実装してみましょう。

実装

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

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() を使い、各メニューでの動作を記述しました。
続いてこれを呼び出す部分です。

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();
  }
}

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


列挙子クラスの共通インターフェース利用

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

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()));
	}

}

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

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;
    }
}

最後にこれの利用方法です。

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 (2019)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0