【Java】Enumの機能を最大限に活かし、可読性を上げる(同時にswitch文も減らす)

Enumとswitch文のイメージ画像 Java

Enumは「種類分け」の観点でよく利用されることがある。
プログラミングの教科書にも、以下のようにswitch文との併用が記載されていることが多い。

public enum CreditCard {
  VISA,
  MASTER,
  JCB,
}
final CreditCard card = CreditCard.VISA;

switch (card) {
  case VISA -> System.out.println("VISAです"); // 出力結果はこれになる
  case MASTER -> System.out.println("MASTERです");
  case JCB -> System.out.println("JCBです");
  default -> System.out.println("該当なし");
}

今回の記事では、これより更にEnumの能力を引き出すための使い方を3つ紹介する。

Enumを何も考えず利用したときの弊害

Enumは便利な機能ではあるが、
何も考えずに使うとただswitch文が増えるだけで、そんなに便利にならない。

また、種別が増えたときにswitch文をあちこち修正しなければいけない手間やリスクがある。

この記事を読むと、副作用としてあまりswitch文を書かずに済むようになる。

超重要な考え方

Enumを活用するために、非常に大切な考え方は
Enumはクラスの一種であり、機能をもたせることができる
ということである。

これを念頭に、例に挙げたCreditCardクラスを拡張してみる。

使い方①:フィールドを持たせてマッピングする

種別情報というのは、DBに数値で入っていることはよくある。

DBの値意味
1VISA
2MASTER
3JCB

このような場合、Enumにフィールドを持たせることで、EnumとDBの値を対応させることができる。

ただし、DBの値→Enumに変更するswitch文を、別のところに書いてしまってはもったいない

Enumの中にそのロジックを記載することで、
「データとそのロジックをひとまとめにする」というオブジェクト指向で実装することができる。

■改善したEnumクラス

@Getter
@RequiredArgsConstructor
public enum CreditCard {
  VISA(1),
  MASTER(2),
  JCB(3),
  UNDEFINED(0); // 「未定義/判定不能」の状態も持っておくとnullチェック地獄にならずに便利

  private final Integer number;

  // 数値からEnum型へ変換を行うメソッド
  public static CreditCard from(final Integer dbValue) {
    return switch (dbValue) {
      case 1 -> VISA;
      case 2 -> MASTER;
      case 3 -> JCB;
      default -> UNDEFINED;
    };
  }
}

■使い方

final CreditCard card = CreditCard.from(1); // 「1」が変換されて「VISA」になる

■補足

分かりやすさのためにfrom()メソッドを一旦switch文で書いたが、
筆者は以下のように書くことが多い。

switch文の場合、クレカ種別が増えたらメソッドも修正が必要だが
以下の書き方であればメソッドの修正漏れがない。

public static CreditCard from(final Integer dbValue) {
  return Arrays.stream(CreditCard.values())
      .filter(v -> Objects.equals(v.getNumber(), dbValue))
      .findFirst()
      .orElse(CreditCard.UNDEFINED);
}

使い方②:メソッドを作ってEnumに判断させる

「VISAを使ったときは10円引き」「MASTERCARDを使ったときは20円引き」
のように、種別に応じて処理を変えたいときもよくある。

このようなケースでも、Enumにロジックを持たせることで
データとロジックが散らばらずに済む。

Enumはクラスの一種であるため、以下のように関数を定義することもできる。

@Getter
@RequiredArgsConstructor
public enum CreditCard {
  VISA(1) {
    @Override
    Integer discount(final Integer price) {
      // VISAは10円引き
      return price - 10;
    }
  },
  MASTER(2) {
    @Override
    Integer discount(final Integer price) {
      // MASTERは20円引き
      return price - 20;
    }
  },
  JCB(3) {
    @Override
    Integer discount(final Integer price) {
      // JCBは30円引き
      return price - 30;
    }
  },
  UNDEFINED(0) {
    @Override
    Integer discount(final Integer price) {
      // 未定義の場合は割引しない
      return price;
    }
  };

  private final Integer number;

  /**
   * 割引した金額を返却する
   *
   * @param price 割引前価格
   * @return 割引後価格
   */
  abstract Integer discount(final Integer price);
}

ただし、この処理の内容が大きくなる場合は後述する③を検討すると良い。

使い方③:Enumの種別に対応するクラスをそれぞれ作成する

Enumにインターフェースを組み合わせると、さらに強力になる。

  • インターフェースの強み:複数の別々のものを同じように扱うことができる
  • Enumの強み:複数の種別を列挙し、管理、識別できる

クラスの数は増えるが、その代わり種別に特化したロジックを拡張性高く書くことができ
利用側の実装コードもかなりシンプルになる。

見たほうが早いので、以下のコードを見てほしい。

■インターフェース

後続するEnumクラスの名前を変更し、インターフェースの名前をCreditCardにした。

public interface CreditCard {

  /**
   * 割引計算をして返却する
   *
   * @param price 割引前価格
   * @return 割引後価格
   */
  Integer discount(final Integer price);
}

■実装クラス

// CreditCardを実装し、VISA専用の機能に特化したクラス
public class VisaCard implements CreditCard {

  @Override
  public Integer discount(final Integer price) {
    return price - 10;  // VISAは10円引き
  }
}

■Enum

分かりやすさのため、名前を変えてCreditCardTypeにした。

@Getter
@RequiredArgsConstructor
public enum CreditCardType {
  // 第二の情報として、CreditCardクラスの実装クラスを格納する
  VISA(1, new VisaCard()),
  MASTER(2, new MasterCard()),
  JCB(3, new JcbCard()),
  UNDEFINED(0, new UnknownCard());

  // マッピングする数値
  private final Integer number;
  // インターフェースでカード情報を持つ
  private final CreditCard card;

  // 数値からEnum型へ変換を行うメソッド
  public static CreditCard from(final Integer dbValue) {
    return Arrays.stream(CreditCardType.values())
        .filter(v -> Objects.equals(v.getNumber(), dbValue))
        .findFirst()
        .orElse(CreditCardType.UNDEFINED)
        .getCard();
  }
}

■使用例

数値からクレジットカード種別を判定し、その結果によって割引後金額を算出する。

final CreditCard card = CreditCardType.from(1);  // 変数cardにはVisaCardクラスのオブジェクトが入っている
final Integer discountedPrice = card.discount(1000);  // VisaCardクラスに書かれているdiscount()メソッドが呼ばれる
System.out.println(discountedPrice);

利用側のコードがかなりスッキリしたことが分かる。

まとめ

  • Enumには種別情報だけではなくロジックそのものを持たせると、オブジェクト指向で実装できる
  • ロジックが肥大化しそうであれば、インターフェースを組み合わせると良い
    • 基準の一つ:引数を受け付けるようなメソッドを種別ごとに書きたくなった場合
  • switch文を書いたとき、この記事を思い出してほしい
タイトルとURLをコピーしました