【Java】定数!?列挙型!?newできない!?特殊なクラスenumの効果的な使い方
こんにちは!あさもっちゃんです!
ご存知の通り、Javaは型を主軸にしたオブジェクト指向言語です。
「型」のオブジェクト指向をやるなら、Javaがいまでもいちばんイケてる言語。https://t.co/tW8F5oGRym
— 増田 亨. (@masuda220) 2018年9月30日
日本でも有名なシステム設計の権威である増田氏の発言にもある通り、 言語が生まれてかなりの時間が経ったのに、今尚色褪せず時代の最先端の設計のしやすさを誇っています。
学ぶには一苦労かもしれませんが、一度知ってしまえば、更に安全に、更に効率よく、更に綺麗に実装ができます! きちんと言語仕様を知ることで、チームでのプログラミングを更に高度なものに昇華できると思います。
このブログでは、Javaの言語仕様を多く取り扱っていく予定です。 読んでくださる皆様には是非ともついてきて頂き、Javaをマスターして頂けたらと思います。
今回はJava特有の最初の難関であるenum(列挙型)について書いていきたいと思います。
enumの使い方 1 - 引数の入力制限
ショッピングサイトの合計支払い金額と配送料をモデルにして説明します。 配送料の状態は「通常500円」「合計金額が2000円以上の時は半額(250円)」「合計金額が5000円以上の時は無料(0円)」の3種類とすると、enumを使わずに丁寧に書くと下記のようなソースコードになります。
import java.math.BigDecimal; public class DeliveryCharge { public static final int NORMAL = 0; public static final int HALF = 1; public static final int ZERO = 2; } public class DeliveryChargeCalculator { private static final BogDecimal ZERO_THRESHOLD = new BigDecimal("5000"); private static final BogDecimal HALF_THRESHOLD = new BigDecimal("2000"); public static int getDeliveryCharge(BigDecimal amount){ if(amount.compareTo(ZERO_THRESHOLD) >= 0) return DeliveryCharge.ZERO; if(amount.compareTo(HALF_THRESHOLD) >= 0) return DeliveryCharge.HALF; return DeliveryCharge.NORMAL; } } public class PaymentCalculator { public static BigDecimal calculate(BigDecimal amount, int charge){ if(charge == DeliveryCharge.ZERO) { return amount; } if(charge == DeliveryCharge.HALF) { return amount.plus(new BigDecimal("250")); } return amount.plus(new BigDecimal("500")); } }
PaymentCalculator.calculateの引数にDeliveryChargeCalculator.getDeliveryChargeで取得した定数を入れて計算を行います。 しかしこの記述では、引数に予定外の「100」とか「-1」とかを入れることが出来てしまいます。 これらが入ってしまうと、DeliveryChargeで定義した定数じゃないのにNORMALの数値で計算してしまうことになります。
ここで、DeliveryChargeの宣言をenumで表現して見ます。
public enum DeliveryCharge { NORMAL, HALF, ZERO; // ここでenumで取り扱える値を宣言する }
これだけです。とてもシンプルですね。
enumの特徴として、クラス宣言の後の定数みたいな宣言以外でこのクラスのインスタンスを作ることが出来ません。 つまり、このクラスを使う人が勝手に別のインスタンスを生成することが出来ないのです。 これを、先ほどのDeliveryChargeCalculator, PaymentCalculatorに適用してリファクタリングすると、
import java.math.BigDecimal; public class DeliveryChargeCalculator { private static final BogDecimal ZERO_THRESHOLD = new BigDecimal("5000"); private static final BogDecimal HALF_THRESHOLD = new BigDecimal("2000"); public static DeliveryCharge getDeliveryCharge(BigDecimal amount){ if(amount.compareTo(ZERO_THRESHOLD) >= 0) return DeliveryCharge.ZERO; if(amount.compareTo(HALF_THRESHOLD) >= 0) return DeliveryCharge.HALF; return DeliveryCharge.NORMAL; } } public class PaymentCalculator { public static BigDecimal calculate(BigDecimal amount, DeliveryCharge charge){ if(charge == DeliveryCharge.ZERO) { return amount; } if(charge == DeliveryCharge.HALF) { return amount.plus(new BigDecimal("250")); } // ここでは必ずchargeはDeliveryCharge.NORMALが入っている return amount.plus(new BigDecimal("500")); }
となります。 こうすれば、PaymentCalculator.calculateの引数に指定できるのはZERO, HALF, NORMALの三つのみとなります。
このように、enumを引数に設定すると、実装した値しか引数に入力できないので、 予期せぬ値の入力による不具合が発生しなくなります。
enumの使い方 2 - メンバ変数を持って処理をわかりやすくする
enumはオブジェクトなので、メンバ変数やメソッドを持つ事ができます。 DeliveryChargeのそれぞれの状態に対して実際の配送料金が一意に決まるので、メンバ変数を持たせて見ましょう。
import java.math.BigDecimal; public enum DeliveryCharge { NORMAL(500), HALF(250), ZERO(0); public final BigDecimal value; private DeliveryCharge(int charge){ this.charge = BigDecimal.valueOf(charge); } }
DeliveryChargeに対応する実際の配送料金が表現できたと思います。 NORMALなら500, HALFなら250, ZEROなら0ですね。 しかし、これではただソースコードの行数が増えて読みづらくなっただけかもしれません。
ここで、PaymentCalculatorを書き換えてみましょう。
import java.math.BigDecimal; public class PaymentCalculator { public static BigDecimal calculate (BigDecimal amount, DeliveryCharge charge){ return amount.plus(charge.value); } }
なんと、PaymentCalculator.calculateメソッド内では、 実際の配送料金がいくらかを意識したロジックを書かなくてもよくなりました!
DeliveryChargeがただのフラグだった場合は、PaymentCalculatorクラス内で DelivaryChargeの業務ロジックを持ってしまっていましたが、DelivaryChargeに内部状態を持つ事で、 PaymentCalculatorクラス内で余計なロジックを記述する必要がなくなりました。
このように、クラス自身がもつ関心事をクラス内に閉じ込めわかりやすい設計にする事ができます。
enumの使い方 3 - abstractメソッドを定義して処理をわかりやすくする
enumはオブジェクトであるので、abstractメソッドも定義できます。 パソコンを購入する時のおすすめ度をabstractメソッドとし、それぞれのおすすめロジックをenum化してみましょう。 (ロジックはテキトーです。)
import java.math.BigDecimal; public enum SelectingLogic { BROWSING(){ @Override bublic double recommendPoint(PCSpec spec){ return ((spec.cpuScore + spec.memoryScore + graphicScore) / price); } }, EDIT_MOVIE(){ @Override bublic double recommendPoint(PCSpec spec){ return ((spec.cpuScore + spec.memoryScore + graphicScore) / price); } }, RECORDING(){ @Override bublic double recommendPoint(PCSpec spec){ return ((spec.cpuScore + spec.memoryScore + graphicScore) / price); } }, COMPOSING(){ @Override bublic double recommendPoint(PCSpec spec){ return ((spec.cpuScore + spec.memoryScore + graphicScore) / price); } }, GAMING(){ @Override bublic double recommendPoint(PCSpec spec){ return ((spec.cpuScore + spec.memoryScore + graphicScore) / price); } }, STOCK_TRADING(){ @Override bublic double recommendPoint(PCSpec spec){ return ((spec.cpuScore + spec.memoryScore + graphicScore) / price); } }; abstract bublic double recommendPoint(PCSpec spec); } public class PCSpec { public double cpuScore; public double memoryScore; public double graphicScore; public double storageScore; public double weightScore; public double sizeScore; public int price; } public class Customer { public SelectingLogic logic; public int budget; } public class Store { private static final double threshold = 60; List<PCSpec> list = Arrays.asList(...); public static List<PCSpec> recommend(Customer customer){ return list.stream().filter({ spec -> spec.price < customer.budget }).filter({ spec -> customer.logic.recommendPoint(spec) > threshold }).collect(Collectors.toList()); } }
このように書く事で、使い方2のようにenumを切り替える事でロジックを切り替える事ができました。 使い方2と同じく、ポリモーフィズムを用いた実装に役立っています。
まとめ
というわけで、enumの使い方をまとめてみました! 使いどころとしては下記でした。
- 引数の入力制限
- 値の切り替え
- メソッドの実装の切り替え
少々わかりづらかったかもしれませんが、自分の手元で実装してみたりすると、意外と理解できたりします。 是非とも一度、enumを使って実装してみてください!!!!
以上、enumの効果的な使い方でした。