こんばんわ, くらたです. 年度改まりまして,プログラミング歴3年目に突入してしまいました. 平成の終わりが, 初心者卒業となることを祈ってがんばります.
今回はEnumの逆引きメソッド(Enumのフィールドをもとにインスタンスを返却するクラスメソッドのことです)について考えるところがあったので徒然なるままに書いていきます.
Enumの逆引きが冗長なので共通化する
追記(2019/04/03)
記事内でInterfaceをImplementsして実現するという趣旨の解説を紹介していましたが, 特にメリットが無い事がわかっています. そのため表題から「Interfaceによって」と読み取れる内容を削除しました.
背景
JavaでEnumを使う際に以下のようなEnum逆引きメソッドを定義することが多いのではないでしょうか.
public enum RegistrationStatus {
// 略
private String code;
public static RegistrationStatus getByCode(String code) {
for (Status status : Status.values()) {
if (status.code.equals(code)){
return status;
}
}
throw new IllegalArgumentException();
}
}
定義したEnumには必ずと言っていいほどわたしは上記のような逆引きメソッドを定義します.
Enumごとに毎回逆引きメソッドを実装する方法には, 2点の問題があるとわたしは思っています.
1. 実装者によって実装の仕方が変わる.
ぱっと考えただけでも2つの観点で実装方法が変わると思っています. 私自身も恥ずかしながら実装するタイミング, レビューするタイミングでどれをOKにするかバラついてしまう次第です.
- for文を使うか, StreamAPIを使うか
- 要素が見つからない場合にNullを返却するのか, Optional.emptyを返却するのか, 例外を投げるのか
2. ソースコードが冗長になる.
いろんなクラスにpublic static ClassName getByCode(String value)...
と定義されるのが少し嫌です.
もしかしたら必要な冗長さなのかもしれませんが, 実装者によって同じ仕様, 実装がバラバラになるのは困ります. なので共通化したいと思った次第です.
サンプルコード
EnumにInterfaceを使ってユーティリティを作成する - システム開発で思うところ
こちらの記事を参考に, 工夫してみました. 当初考えていたことのほとんどがこの記事で実現されています. 本当にありがとうございます.
EnumReverseLookupable.java
検索条件を引数にして, 検索のロジックはInterfaceのメソッドに閉じ込めました.
Interface内でStreamAPIを使うだの, 検索結果がNullだったときの処理をどうするかの問題を解決しています.
参考記事では予め
interface EnumReverseLookupable {
public static <E extends Enum<E>> E getByCondition(Class<E> enumClass, Predicate<E> p) {
return Arrays
.stream(enumClass.getEnumConstants())
.filter(p)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException());
}
}
RegistrationStatus.java
Enum実装者はInterfaceのfilterの条件を指定するだけ.
public enum RegistrationStatus implements EnumReverseLookupable {
TEMPORARY("0", "仮会員"), REGULAR("1", "正会員");
private String code;
private String text;
private RegistrationStatus(String code, String text) {
this.code = code;
this.text = text;
}
public String getCode() {
return code;
}
public String getText() {
return text;
}
// 呼び出し側は, InterfaceのstaticメソッドをCallするだけ. 引数にEnumの検索条件を無名関数で指定.
public static RegistrationStatus getByCode(String value) {
return EnumReverseLookupable.getByCondition(RegistrationStatus.class, (RegistrationStatus e) -> e.code.equals(value));
}
}
サンプルコード2
追記(2019/04/03)
Twitterにてアトバイスをいただきましたので, その内容を追記します.
ここで挙げられている問題点、まさしくJavaがstaticメソッドを共通化して定義できない問題から来てますね。ScalaとかKotlinであればコンパニオンオブジェクトにmixinが可能なのでここで挙げられてる問題が無くなってしまいます https://t.co/1a90hDk6dW
— がくぞ (@gakuzzzz) 2019年4月2日
あーまぁなんでJavaでの解決の方法もそれに準じたアプローチもとれますよという感じですね。 https://t.co/jxrVZ9uLo0
— がくぞ (@gakuzzzz) 2019年4月2日
メソッドのシグネチャ(仕様)を定義するのが責務であるInterfaceではなく, Classを使って実現をしているようです.
以下ソースコードはgistより引用
/**
* EnumのクラスとGetterから逆引き検索関数を生成するためのクラス.
*/
public class EnumReverseLookup<E extends Enum<E>, ATTR> {
private final Class<E> enumClass;
private final Function<E, ATTR> getter;
public EnumReverseLookup(final Class<E> enumClass, final Function<E, ATTR> getter) {
this.enumClass = enumClass;
this.getter = getter;
}
public E lookup(ATTR attribute) {
return Arrays
.stream(enumClass.getEnumConstants())
.filter(e -> getter.apply(e).equals(attribute))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException());
}
}
public enum RegistrationStatus {
TEMPORARY("0", "仮会員"), REGULAR("1", "正会員");
private String code;
private String text;
private RegistrationStatus(String code, String text) {
this.code = code;
this.text = text;
}
public String getCode() {
return code;
}
public String getText() {
return text;
}
/*
* EnumReverseLookupクラスのコンストラクタに自クラスとGetterを渡して, 逆引き検索関数を生成する.
*/
public static final EnumReverseLookup<RegistrationStatus, String> byCode =
new EnumReverseLookup<>(RegistrationStatus.class, RegistrationStatus::getCode);
}
public class Hoge {
void hoge() {
RegistrationStatus.byCode.lookup("0");
}
}
検討すべき新たな課題
EnumReverseLookupable#getByCondition
の呼び出しを制限する.
サンプルで定義したEnumReverseLookupable#getByCondition
をEnumではないクラスから呼び出されると, プログラムのメンテナンス性が下がるのではと考えています.
RegistrationStatusを逆引きしたいHoge.java
の例を考えると
public class Hoge {
void hoge() {
RegistrationStatus.getByCode("0");
}
}
public class Hoge {
// RegistrationStatusに定義したメソッドを使わず, 冗長なコードを書いてしまう.
void hoge() {
EnumReverseLookupable.getByCondition(RegistrationStatus.class, (RegistrationStatus e) -> e.getCode().equals("0"));
}
}
この課題を解決するためには, 以下の2つを施策として検討したほうが良いです.
1. Interfaceをパッケージプライベートにして, 同一パッケージにEnumを集める.
Interfaceを同一パッケージからのみ参照可能にすると, 他パッケージからのInterfaceのメソッド呼び出しを制限できます. サンプルコードはこれを採用しました.
2. コーディング規約でEnumReverseLookupable.getByCondition
の呼び出しを制限
「1を採用したいのはやまやまだけど, Enumを1つのパッケージにまとめるのって結構厳しい」という場合はこの方法を採用するとよいでしょう.
最後に
本当はLombokLikeに@EnumSearchKey
と指定したフィールドの名前で動的にメソッドを生成するまでやりたかったです. でもわたしの力が及びませんでした. こっちのほうがきれいだと思うので, 実現を目指しています.
あとはJavaのemumのエレガントな使い方|teratailを読んで, そもそもフィールド無いほうが幸せなんじゃないかと思ったので, それについてもまとめられればと思います.