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

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

アクターとユースケース(権限)の実装

vermeer.hatenablog.jp

を元に アクターとユースケース(権限)の実装をしてみました。
まだサービスやコントローラーへの適用はしていません。

想定する要件

アクターが複数あって、それぞれに権限がありそうなものということで「申請フロー」を題材にしました。

状態遷移図

f:id:vermeer-1977:20190302104539p:plain

ユースケース

f:id:vermeer-1977:20190302104506p:plain

「編集」については、状態変更を伴わないということもあり 除外して、それ以外の 状態遷移のトリガーとなる振る舞いを 基本的に そのままユースケースとして採用しました。

ツッコミどころや補足など

申請者と承認者を継承関係にしていますが、正直 自分自身「うーん」と思っています。 要件としての役割だから継承関係にしない方が正しい整理だと思うんですよね。 継承すべきものがあるとしたら「社員」みたいなアクターのような気もするんですけど、今回はアクターの継承を含んだ実装例を試したかったということで目をつぶってください。

状態が終了することは無いの?という状態遷移になっていますが、これは一応、意図的なものです。 イミュータブルデータモデル的なことを想定していて、すべての状態を履歴として管理する、という意図です。

実装(インターフェース)

アクター

public interface ActorType {

    public Set<UsecaseType> usecases();

    public default ActorType parentActor() {
        return null;
    }

    public default Set<UsecaseType> recursiveScan(ActorType actor, Set<UsecaseType> usecases) {
        if (actor == null) {
            return usecases;
        }
        usecases.addAll(actor.usecases());
        return recursiveScan(actor.parentActor(), usecases);
    }

    public default boolean hasUsecase(UsecaseType usecase) {
        return this.usecases().contains(usecase);
    }

    public default boolean anyMatch(Set<UsecaseType> usecases) {
        return this.usecases().stream().anyMatch(usecase -> usecases.contains(usecase));
    }

}

ユースケース

ユースケースは、アクターと違って 永続層などの値から ユースケースEnumへの変換が必要だというところです。 ということで、変換用のユーティリティとしても 本インターフェースを使います*1

public interface UsecaseType {

    public Object code();

    public static <E extends Enum<E> & UsecaseType> Optional<E> codeOf(Class<E> enumType, Object code) {
        return Arrays.stream(enumType.getEnumConstants())
                .filter(e -> e.code().equals(code))
                .findFirst();
    }

}

実装(具象クラス)

実装例ということで、分かりやすさ重視で 上述の ユースケース図や状態遷移図の日本語表現を そのまま使っています。 実際の作成の順番としては、ユースケースEnumをつくって、アクターのEnumを実装します。

アクター

public enum Actor implements ActorType {
    起票者(起票, 申請, 取下),
    承認者(差戻, 承認) {
        @Override
        public Actor parentActor() {
            return 起票者;
        }
    };

    private final Set<UsecaseType> usecaces;

    @Override
    public Set<UsecaseType> usecases() {
        return this.usecaces;
    }

    private Actor(UsecaseType... usecases) {
        Set<UsecaseType> _usecases = (usecases == null || usecases.length == 0)
                                     ? Collections.emptySet()
                                     : new HashSet<>(Arrays.asList(usecases));
        this.usecaces = this.recursiveScan(this.parentActor(), _usecases);
    }

}

承認者は、起票者を拡張したアクターであることを parentActorで表現しています。
あとは各アクターに関連するユースケースを記述するだけです。

再帰的にアクターに紐づくユースケースを取得するためにコンストラクタでthis参照をしているところが、ちょっと気にはなっています。
インスタンスフィールドを参照しているわけではないので、大丈夫かなぁと思っていますが、もし問題があるようなら、アクターに関連付けされているユースーケースを取得するメソッド(hasUsecase)で都度 階層情報をさかのぼって ユースケース情報を参照するようにしないといけないかもしれません。一応、ユニットテストでは 想定通りの挙動をしています。

ユースケース

public enum Usecase implements UsecaseType {
    起票, 申請, 取下, 差戻, 承認;

    @Override
    public Object code() {
        return this.name();
    }

    public static Optional<Usecase> codeOf(Object code) {
        return UsecaseType.codeOf(Usecase.class, code);
    }

}

永続層などで保持しているコード値はUsecaseのEnum列挙子と同値という前提です*2
Enumクラスの valueOfの コード値版として、static な codeOfメソッドを準備します*3

使い方の例(テストコード)

public class ActorTest {

    @Test
    public void hasUsecasesAt起票者() {
        Set<UsecaseType> usecases = Actor.起票者.usecases();
        Set<UsecaseType> expects = new HashSet<>(Arrays.asList(起票, 申請, 取下));
        Assert.assertTrue(usecases.containsAll(expects));
    }

    @Test
    public void hasUsecasesAt承認者() {
        Set<UsecaseType> usecases = Actor.承認者.usecases();
        Set<UsecaseType> expects = new HashSet<>(Arrays.asList(起票, 申請, 取下, 差戻, 承認));
        Assert.assertTrue(usecases.containsAll(expects));
    }

    @Test
    public void hasUsecase() {

        Assert.assertTrue(Actor.起票者.hasUsecase(取下));
        Assert.assertTrue(Actor.承認者.hasUsecase(取下));

        Assert.assertFalse(Actor.起票者.hasUsecase(差戻));
    }

    @Test
    public void anyMatch() {
        Set<UsecaseType> items = new HashSet<>(Arrays.asList(差戻, 承認));
        Assert.assertFalse(Actor.起票者.anyMatch(items));

        Assert.assertTrue(Actor.承認者.anyMatch(Actor.起票者.usecases()));

    }

    @Test
    public void convertUsecaseEnum() {
        Assert.assertEquals(Usecase.codeOf("取下").get(), Usecase.取下);
    }

}

さいごに

Actorとユースケースの実装については、だいたいこんな感じで良いかな と思います。
次は これらのEnumを実際に サービスやコントローラーで使ってみて、使い勝手を検証していきたいと思います。


*1:EnumのInterfaceに共通処理を実装する - システム開発で思うところ に 少しStreamでアレンジを加えて実装しました。

*2:手抜きですが意図は伝わると思います

*3:実際の実装に近づけたいので こちらは手抜きをしません