システム開発で思うところ

JavaEEを主にシステム開発をしながら思うところをツラツラを綴る

FactoryはEnumで実装すると良さそう

Enum活用への提案

ヒントになったのは「現場で役立つシステム設計の原則」。

書評でも良かったところとして挙げた「ロジックをenumを使って表現する」のところ。

vermeer.hatenablog.jp

Enumにロジックを表現する、という例としては Strategyがあるようです。

【enum】メソッドの定義(3)−strategyパターンを使う方法 - THE HIRO Says

StrategyをEnumに持たせてはいけない理由 - にょきにょきブログ

後者では、気を付けておくべきこと、ということとしてEnumのStrategyを説明されているようですが、懸念されているところについては Factoryであれば問題ないように思います(たぶん)。

想定ケース

画面から入力された値に応じたクラスを生成したい。

生成されるクラス

生成クラス用インターフェース

public interface GenderInterface {

    public void action();
}

具象クラス

public class Male implements GenderInterface {

    @Override
    public void action() {
        System.out.println("Male!!");
    }

}
public class Female implements GenderInterface {

    @Override
    public void action() {
        System.out.println("Female!!");
    }

}

通常のFactory(Enumは列挙子のみ)

Enum

public enum GenderType {
    MALE, FEMALE
}

Factory

    public static GenderInterface create(GenderType gender) {

        switch (gender) {
            case MALE:
                return new Male();
            case FEMALE:
                return new Female();
            default:
                throw new IllegalArgumentException("gender code invalid");
        }
    }
}

使用例

public void orthodoxFactoryMethod() {
    GenderFactory.create(GenderType.valueOf("MALE")).action();
    GenderFactory.create(GenderType.valueOf("FEMALE")).action();
}

所感

画面側の値をEnumの列挙子名に変換することが必要になるので これだったら、そもそもEnumを使用しない方が良いと思う。

通常のFactory(EnumにStrategy)

Enumに判定用の定数とStrategyクラスを設定して、Factoryクラスは別に作成したケース*1

StrategyをEnumで保持していないところを除けば、今回の実験前の私の実装に近いものです。

Enum

public enum GenderTypeWithStrategy {
    MALE(0, new Male()),
    FEMALE(1, new Female());

    private final Integer genderCode;
    private final GenderInterface gender;

    private GenderTypeWithStrategy(Integer genderCode, GenderInterface gender) {
        this.genderCode = genderCode;
        this.gender = gender;
    }

    public Integer genderCode() {
        return this.genderCode;
    }

    public GenderInterface getGender() {
        return this.gender;
    }

}

Factory

public class GenderEnumStrategyFactory {

    public static GenderInterface create(Integer genderCode) {

        if (Objects.equals(GenderTypeWithStrategy.MALE.genderCode(), genderCode)) {
            return GenderTypeWithStrategy.MALE.getGender();
        }

        if (Objects.equals(GenderTypeWithStrategy.FEMALE.genderCode(), genderCode)) {
            return GenderTypeWithStrategy.FEMALE.getGender();
        }

        throw new IllegalArgumentException("gender code invalid");
    }
}

使用例

public void enumStrategyFactoryMethod() {
    GenderEnumStrategyFactory.create(0).action();
    GenderEnumStrategyFactory.create(1).action();
}

所感

Enumで区分値とStrategyを管理するので、情報の集約は出来ている感じ。

ただ Factoryの方をもう少しすっきり実装したい。

今回の実験前だと、私はreturnのところに 各インスタンス生成ロジックを記述していました。

EnumでFactory(パラメータ無し)

Enum Factory

public enum Gender {
    MALE(0, new Male()),
    FEMALE(1, new Female());

    private final Integer genderCode;
    private final GenderInterface gender;

    private Gender(Integer genderCode, GenderInterface gender) {
        this.genderCode = genderCode;
        this.gender = gender;
    }

    public static GenderInterface of(Integer genderCode) {
        for (Gender genderType : Gender.values()) {
            if (Objects.equals(genderType.genderCode, genderCode)) {
                return genderType.gender;
            }
        }
        throw new IllegalArgumentException("gender code invalid");
    }
}

使用例

public void enumFactoryMethod() {
    Gender.of(0).action();
    Gender.of(1).action();
}

所感

情報と振る舞いが集約された。

区分値が増えても生成ロジック部分(of)に手を加えなくて良い。

EnumでFactory(パラメータあり)

(2017/11/4 追記)

生成されるクラス

生成クラス用インターフェース

public interface SexInterface {

    public <T extends SexInterface> T create(String name);

    public void action();
}

具象クラス

public class Man implements SexInterface {

    private String name;

    protected Man() {
    }

    protected Man(String name) {
        this.name = name;
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T extends SexInterface> T create(String name) {
        return (T) new Man(name);
    }

    @Override
    public void action() {
        System.out.println("Man:" + this.name);
    }

}
public class Woman implements SexInterface {

    private String name;

    protected Woman() {
    }

    protected Woman(String name) {
        this.name = name;
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T extends SexInterface> T create(String name) {
        return (T) new Woman(name);
    }

    @Override
    public void action() {
        System.out.println("Woman:" + this.name);
    }

}

Enum Factory

public enum Sex {
    MAN(0, new Man()),
    WOMAN(1, new Woman());

    private final Integer sexCode;
    private final SexInterface sex;

    private Sex(Integer sexCode, SexInterface sex) {
        this.sexCode = sexCode;
        this.sex = sex;
    }

    public static SexInterface of(Integer sexCode, String name) {
        for (Sex sexType : Sex.values()) {
            if (Objects.equals(sexType.sexCode, sexCode)) {
                return sexType.sex.create(name);
            }
        }
        throw new IllegalArgumentException("sex code invalid");
    }

    public boolean isAssignableFrom(SexInterface sex) {
        return this.sex.getClass().isAssignableFrom(sex.getClass());
    }

}

使用例

public void enumFactoryMethodWithParam() {
    Sex.of(0, "taro").action();
    Sex.of(1, "hanako").action();
}

所感

生成時に情報を付与してインスタンスを生成することもできます。

どちらかというと、パラメータを使っている この例の方が よりFactory感がある気がしますので追記しました*2

追記(2017/11/5)

メソッドisAssignableFrom を追加しました*3

比較ロジックもEnum側で実装しておけば、コード内の比較ロジックも読みやすくなるかな?と思います。

例えば男女インスタンスをリストに格納して、あとから逐次判定をしたいときとかに使うイメージです。

ドメイン情報の漏れ出しにつながりやすいところだとは思いますが、個々のロジックに比較を実装するよりは統一的な操作を提供するやり方の方が前向きな気がします。

少なくとも、このやり方であれば 区分値の追加時にEnum列挙子の追加だけで実装側に比較ロジックを実装する必要はなくなります。

インスタンス型判定のメソッド

生成したインスタンスを比較したい場合に使用する共通的なメソッドです。

使用例

public void isAssignableFrom() {
    SexInterface man = Sex.of(0, "taro");
    SexInterface woman = Sex.of(1, "hanako");

    Assert.assertThat(Sex.MAN.isAssignableFrom(man), is(true));
    Assert.assertThat(Sex.WOMAN.isAssignableFrom(woman), is(true));
}

Code

https://bitbucket.org/vermeer_etc/enum-factory

さいごに

EnumでFactoryを実装しようと思った動機としては、区分値判定を減らしたいな、もしくは集約させたいな、と思ったからです。

言い方は良くないですが、集約すること自体を目的とした実験をしてみよう、というのを動機として やってみたら悪くは無さそうな実装例だったので公開したという感じです。

*1:Factoryクラス名が変なのは違いを表現するためなのでスルーしてください

*2:というか、自分の実装の実験としてはパラメータ有りである必要があっただけですが それを失念していただけです

*3:あとtypoも…