はじめに
タイトル通り、先日分からされてしまったので、Enumの勉強をしました。
Enumなかなか奥が深いです。よければ読んでみてください。
〇 想定読者
- Enumについては参考書などでさらっと見ただけ、という人
- 「Enum?定数の集まりみたいなやつよね?」という人
- Enum分かったかも!という気になりたい人
- 「Enum、好きだ。」という人
- Enum使おうと思ったけどEnumに分からされた人
〇前提
- 記事で扱う言語はJavaです
- 記事に残すのは勉強した一部ですのでご了承ください
- enumよりEnum派です、たぶん一般的なのはenumです
目次
Enumを理解する
なんでEnumを使うのか
マネージャーが以下のようなことを教えてくれました。
Enumのメリットは「上限のあるものを定義できる」点にあると思っています。
たとえば信号は赤・青・黄しかありませんが、enumではなくVoやDtoにすると、エラーの実装を自分で入れない限り無限に表現できます。
たとえば、黒・白・紫など、色なら何でも受け入れられるObjectになりますが、enumであれば赤・青・黄だけを列挙して定義しておけば、他のものになりようがないので、必要以上の自由度を与えない、安全なObjectが表現できると思っています。
システムの例で言えばコンボボックスや選択式で入力を許容された設定とかはenumで表現するのに適したObjectだと思います。
また保守性の観点から考えた場合、オプションが1コ増えた、1コ減ったとかの場合でもenumの列挙している定義を1コ減らすか増やすかで済むので使いやすい、機能追加しやすいObjectになります。
そのため、適切な場合には選択すべきと考えます。
自分が店員だとして、売っているのがMEVIUS・PEACE・MARLBOROの3銘柄なのにお客さんから「わかば」って言われたらグーパンreturnしそうになるけど、enumで定義されてたら即ご退店願えるということか!
1.列挙型として定義されるため、コンパイル時に値の検証が可能で、予期しない値を防止できる。
2.名前付きの値を使用することで、コードの意図が明確に伝わり、可読性が向上する。
Enumさんの基本的な使い方
基本系
public enum SizeUtils {
SMALL,
MEDIUME,
LARGE;
// SMALL, MEDIUM, LARGE の横並びでもOKです
}
定数自身に値を持たせるパティーン
public enum SizeUtils {
SMALL("小さい"),
MEDIUM("普通"),
LARGE("大きい");
private final String size;
private SizeUtils(String size) {
this.size = size;
}
}
定数自身に値を持たせるパティーン(2つ以上も可)
public enum SizeUtils {
SMALL("小さい", 1),
MEDIUM("普通", 2),
LARGE("大きい", 3);
private final String size;
private final int value;
private SizeUtils(String size, int value) {
this.size = size;
this.value = value;
}
}
クラス内クラスとして定義するパティーン
public class DrinkMenu {
// 変数宣言や処理など
public enum SizeUtils {
SMALL("小さい", 1),
MEDIUM("普通", 2),
LARGE("大きい", 3);
private final String size;
private final int value;
private SizeUtils(String size, int value) {
this.size = size;
this.value = value;
}
}
}
定数を取得する基本はgetメソッド
public enum SizeUtils {
SMALL("小さい", 1),
MEDIUM("普通", 2),
LARGE("大きい", 3);
private final String size;
private final int value;
private SizeUtils(String size, int value) {
this.size = size;
this.value = value;
}
public String getSize(){
return this.size;
}
public int getValue(){
return this.svalue;
}
}
上記までで基本的な使い方を見ましたが、Enumクラス内には任意のフィールドやメソッドを追加・定義できるので、いろいろ調べてみてください!
Enumを使ってみる(マジックナンバーからEnumへの修正例)
以下のコードをEnumを使って修正していきます。
// 修正用のサンプルクラスです
public class DrinkMenu {
// ドリンクサイズによって値段を返す
public int drinkPrice(int size) {
if (size == 1) {
// Sサイズの場合
return 150;
} else if (size == 2) {
// Mサイズの場合
return 250;
} else {
// Lサイズの場合
return 400;
}
}
//ほかのメソッドなど
}
現状のコードの問題点
・初見で見た人が「1ってなに?1,2,3以外の数はわたってこないの?」となる(JavaDoc書こう)
・意図せずsizeの値が1, 2, 3以外に書き変わっても全部400が返され、バグに気が付きにくい
・XLサイズが追加されたときにif分岐を増やさないといけない(もしかしたらこのクラス以外も)
・値段が改定されたらそれぞれ書き換えないといけないので修正が漏れるリスク
ステップ1:Enumクラスを作成する
ここにDrinkMenu.drinkPriceメソッドで使うドリンクサイズを定義しちゃいます
public enum SizeUtils {
SMALL,
MEDIUM,
LARGE;
}
ステップ2:sizeに応じた値をセットして、値の取得メソッドつくる
public enum SizeUtils {
SMALL(1), MEDIUM(2), LARGE(3);
private final int size;
// コンストラクタはprivateでなければいけない
private SizeUtils(int size){
this.size = size;
}
public int getSize(){
return this.size;
}
}
コンストラクタはprivateであるべき
一度定義されたEnumの定数を外部から変更不可能にするため。
Enumの定数は、コンパイル時に静的なインスタンス(※)として生成されるため、Enumの不変性と一貫性を確保するためにもコンストラクタはprivateにしておく。
※ 静的なインスタンス = プログラムが実行される前に生成され、プログラムの実行中に変更されることなく存在し続けるインスタンス
ステップ3:DrinkMenuクラスのdrinkPriceメソッドのマジックナンバーを消す
条件判定の部分で、マジックナンバー(1とか2)だった箇所をEnumの定数に置き換えました。
マジックナンバー
・コードに直接記述された数字で、初見では「はにゃ?」となる数字。
・定数や意味のある名前を使うことで、コードの可読性と保守性が向上する。
public class DrinkMenu {
/**
* サイズによって謎の値を返す
* @param size S = 1 / M = 2 / L = 3
* @return サイズごとに決められた値段
*/
public int drinkPrice(int size) {
if (size == SizeUtils.SMALL.getSize()) {
// Sサイズの場合
return 150;
} else if (size == SizeUtils.MEDIUM.getSize()) {
// Mサイズの場合
return 250;
} else {
// Lサイズの場合
return 400;
}
}
//ほかのメソッドなど
}
sizeによって返す値が決まっているなら、返す値もEnumでまとめられそう!
ステップ4:コンストラクタ引数にドリンクの値段も加える
import java.util.Arrays;
public enum SizeUtils {
SMALL(1, 150), MEDIUM(2, 250), LARGE(3, 400)
private final int size;
private final int price;
private SizeUtils(int size, int price){
this.size = size;
this.price = price;
}
public int getSize(){
return this.size;
}
public int getPrice(){
return this.price;
}
/**
* SizeUtilsのすべての定数を配列として取得し、配列の要素を順にチェックする
* 指定された値と一致するSizeUtilsのインスタンスが見つかればそれを返し、
* 一致するものがなければIllegalArgumentExceptionを返す
* @param size ドリンクのサイズ
* @return value 定数
*/
public static SizeUtils of(int size) {
return Arrays.stream(values())
.filter(e -> e.getSize() == size)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("指定されたサイズに対応するEnumが存在しません:" + size));
}
}
以下、enumクラスのSizeUtils.java内に定義したofメソッドについて
以下のコードと同義です。
// SizeUtilsのすべての定数を配列として取得し、拡張for文を使って配列の要素を順にチェックする
// 指定された値と一致するSizeUtilsのインスタンスが見つかればそれを返し、一致するものがなければIllegalArgumentExceptionを返す
public static SizeUtils of(int size) {
for (SizeUtils value : values()) {
if (value.getSize() == size) {
return value;
}
}
throw new IllegalArgumentException("指定されたサイズに対応するEnumが存在しません: " + size);
// NOTHINGという列挙型を作っておいて、以下のようにreturnするのもアリ
// return NOTHING;
}
このofメソッド(命名はなんでもいいです)でドリンクサイズに一致する定数を返してくれるので、
その定数が持っている設定値を取得すればもともとあった条件分岐を消せる!
ステップ5:DrinkMenu.drinkPriceを書き換える
public class DrinkMenu {
/**
* サイズによって謎の値を返す
* @param size S = 1 / M = 2 / L = 3
* @return サイズごとに決められた値段
*/
public int drinkPrice(int size) {
// ここで引数sizeがどの定数に該当するか判別
SizeUtils drinkSize = SizeUtils.of(size);
// 定数の第2引数を取得して返すだけ
return drinkSize.getPrice();
}
//ほかのメソッドなど
}
もともとあった条件分岐が消えてすっきりしました!
もしここでドリンクのサイズにXLが加わったり、値段が改定されたとしても、
Enumを使うようにコーディングしておくことで、Enumクラスの定数を増やす or 定数の値を書き換えるだけで済むので変更がめちゃ楽です。
また、今回の例では触れていませんが、DrinkMenuクラスでドリンクサイズを用いた処理がほかにある場合は、EnumのSizeUtilsクラスにまとめておくことも検討するべきです。ロジックをまとめておくことで、例えば別のクラスで同じロジックを書く必要がなくなり、凝集度が高まるからです。
参考:ドメイン駆動設計入門 Chapter 2 システム固有の値を表現する「値オブジェクト」のまとめと感想
おわりに
やっぱり自分で書いてみて詰まって調べて書いてを繰り返して、初めて身に付きますね。
この記事が誰かのためになれたらうれしく思います!