BETA

ドメイン駆動設計にチャレンジ(ドメインのモデル、エンティティ部分の実装)

投稿日:2019-08-29
最終更新:2019-08-29

ドメイン駆動設計で、今までやってきたことをもとにちまちまコード書いていたのですが・・・まあ、こんなものなのかなーというところまできました。

ドメインの中核をどんな感じで表せば良いのかな?という目的でソースを書いたので、application,repository,serviceは全然書いていません。
今まで書いてきたこととあっていない部分もあります。

ただ、今回はまあ自分の目的を果たせたので、とりあえずはこんなものかな、と思い、今回はここで打ち止めにしておきます。
ソースはここです。

パッケージ構成

パッケージ構成は、

  • application
  • model
  • model.specification
  • service
  • infrastracture
    となっています。

中心となるのはmodelパッケージになります。modelパッケージの中に、集約ルートのエンティティとなるEntryFrame.javaがあり、ファクトリとしてEntryFrameCapacity.java、リポジトリのインターフェースIEntryFrameReposotory.javaがあります。

model.specificationパッケージは、検証の仕様を表すためのIEntryFrameContdition.javaインターフェースと生成の仕様を表すためのIEntryFrameRequires.javaインターフェースがあり、業務ルールを実現する上で必要な仕様があるときは、これらのインターフェースを実装します。

ドメインの中核となるEntrytryFrame.java

EntityFrame.javaの元になるクラス図は以下となります。

EntityFrame.javaは、エントリ枠の集約ルートとしています。エンティティでもあります。
EntityFrame.javaは、

  • 参加申し込み
  • 先着順の参加者確定
  • 抽選
    をpublicとし、その他エントリ枠の状態を募集中や抽選中のステータスに変更する、という操作はpackage privateにしました。

部分的に抜粋すると、参加申し込のメソッドは、
EntryFrame.java

    public void acceptParticipant(ParticipantAcceptanceCategorySpecification participantAcceptanceCategorySpecification,  
            ParticipantAcceptanceMemberSpecification participantAcceptanceMemberSpecification, MemberId memberId,  
            Today today) {  
        if (!participantAcceptanceCategorySpecification.isSatisfiedBy()) {  
            throw new IllegalStateException();  
        }  
        if (!participantAcceptanceMemberSpecification.isSatisfiedBy()) {  
            throw new IllegalStateException();  
        }  

        ApplicationDate applicationDate = new ApplicationDate(today.getFormattedToday());  
        Participant participant = new Participant(memberId, applicationDate);  
        participants.addParticipant(participant);  

    }  

変数名が長いですが、、、IDE力任せでやってしまったので。。。

        if (!participantAcceptanceCategorySpecification.isSatisfiedBy())   

のif文は、エントリの状態が「募集中」で、エントリ枠が「先着順」で、まだ定員に達していない、という業務ルールを満たしているか、を判定しています。

        if (!participantAcceptanceMemberSpecification.isSatisfiedBy())  

は、参加希望者がすでにエントリ枠に参加申し込みを行っていないか、他のエントリ枠に存在しないかという業務ルールを満たしているか、を判定しています。

それぞれのif文、というか、仕様を表すクラスなのですが、関連するようないくつかのルールを1つのクラスにまとめたりしてしまったので、イマイチかもしれません。仕様パターンを使えば良いかと思いましたが、とりあえずはこんな感じにしました。

ちなみに、participantAcceptanceMemberSpecificationクラスの中はこんな風になっています。

/**  
 * 参加申し込み時に会員の参加条件の仕様を表す。  
 */  
public class ParticipantAcceptanceMemberSpecification implements IEntryFrameConditionSpecification {  

    private Participants participants;  
    private List<Participants> otherSportParticipants;  
    private MemberId applyingMemberId;  

public ParticipantAcceptanceMemberSpecification(Participants participants,  
            List<Participants> otherSportParticipants, MemberId applyingMemberId) {  
        this.participants = participants;  
        this.otherSportParticipants = otherSportParticipants;  
        this.applyingMemberId = applyingMemberId;  

    }  

    @Override  
    public boolean isSatisfiedBy() {  
        if (participants.isAlreadyAccepted(applyingMemberId)) {  
            return false;  
        }  

        if (otherSportParticipants.stream().anyMatch(e -> e.isAlreadyAccepted(applyingMemberId))) {  
            return false;  
        }  

        return true;  
    }  

あんまり大したことはしていませんが、エントリ枠が持つ参加希望者の集合に、参加を申し込もうとしている会員がいないか、あとは他のエントリ枠にすでに参加申し込みをしていないかを確認しています。

参加者申し込みのメソッドの後半、

        ApplicationDate applicationDate = new ApplicationDate(today.getFormattedToday());  
        Participant participant = new Participant(memberId, applicationDate);  
        participants.addParticipant(participant);  

参加申し込み日の値オブジェクトを作成して、会員を表す値オブジェクトとともに参加希望者の値オブジェクトを作成し、参加希望者の集合であるparticipantsに追加しています。
particitapntsはファーストクラスコレクションとしています。

Participants..javaは、こんな感じになっています。

/**  
 * 参加希望者のリストを表す。  
 */  
public class Participants {  
    private List<Participant> participants = new ArrayList<>();  

    Participants() {  
    }  

    /**  
     * 参加希望者を追加する。すでに参加済みの場合は参加希望者に追加しない。  
     *  
     * @param participant 参加希望者  
     */  
    void addParticipant(Participant participant) {  
        if (participant == null) {  
            throw new IllegalArgumentException();  
        }  
        participants.add(participant);  
    }  

    /**  
     * 参加希望者の人数を返却する。  
     *  
     * @return 参加希望者の人数  
     */  
    public int getNumberOfPeople() {  
        return participants.size();  
    }  

    /**  
     * 参加希望者がすでに参加していないか確認する。  
     *  
     * @param participant 参加希望者  
     * @return 参加希望者が参加している場合にtrue  
     */  
    public boolean isAlreadyAccepted(MemberId applyingMemberId) {  
        if (applyingMemberId == null) {  
            throw new IllegalArgumentException();  
        }  
        return participants.stream().map(e -> e.getMemberId()).anyMatch(e -> e.equals(applyingMemberId));  
    }  

Listでもいいかなーと思ったのですが、ファーストクラスコレクションにしてみました。ファーストクラスコレクションですが、呼び元、ここではEntryFrameの参加申し込みのメソッドですが、Listという生のデータ構造ではなく、参加希望者の集合として表すことにより、ソースの見通しがよくなり、より業務ルールをコードで表現できるようになったのではないか、と感じました。

次は抽選のメソッドです。

    public void lottery(Today today, LotteryConfirmationSpecification lotteryConfirmationSpecification) {  

        if (!lotteryConfirmationSpecification.isSatisfiedBy()) {  
            throw new IllegalStateException();  
        }  

        confirmedParticipants = confirmationParticipantsCategory.confirmParticipants(participants, entryFrameCapacity);  

        entryFrameStatus = EntryFrameStatus.CONFIRMED;  
    }  

メソッドの先頭でやっているのは参加申し込みのメソッド同様、抽選ができるかを確認しています。

参加者を確定しているところは、

        confirmedParticipants = confirmationParticipantsCategory.confirmParticipants(participants, entryFrameCapacity);  

ですが、実際の参加者確定処理は先着順抽選区分を表すenumで行っています。
ConfirmationParticipantsCategory.java

/**  
 * 先着順抽選区分を表す。  
 */  
public enum ConfirmationParticipantsCategory {  
    /**  
     * 区分値:先着順を表す。  
     */  
    FIRST_ARRIVAL() {  
        @Override  
        public ConfirmedParticipants confirmParticipants(Participants participants,  
                EntryFrameCapacity entryFrameCapacity) {  
            List<Participant> collect = participants.stream().limit(entryFrameCapacity.getEntryFrameCapacity())  
                    .collect(Collectors.toList());  
            ConfirmedParticipants confirmedParticipants = collect.stream().collect(ConfirmedParticipants::new,  
                    (container, participant) -> container  
                            .addConfirmedParticipant(new ConfirmedParticipant(participant)),  
                    ConfirmedParticipants::addAll);  
            return confirmedParticipants;  
        }  
    },  
    /**  
     * 区分値:抽選を表す。  
     */  
    LOTTERY() {  
        @Override  
        public ConfirmedParticipants confirmParticipants(Participants participants,  
                EntryFrameCapacity entryFrameCapacity) {  
            List<Participant> collect = participants.stream().collect(Collectors.toList());  
            Collections.shuffle(collect);  
            List<Participant> subList = collect.subList(0, entryFrameCapacity.getEntryFrameCapacity());  
            ConfirmedParticipants confirmedParticipants = subList.stream().collect(ConfirmedParticipants::new,  
                    (container, participant) -> container  
                            .addConfirmedParticipant(new ConfirmedParticipant(participant)),  
                    ConfirmedParticipants::addAll);  
            return confirmedParticipants;  

        }  
    };  

    /**  
     * 参加希望者から、参加者を確定する。  
     *  
     * @param participants       参加希望者  
     * @param entryFrameCapacity 定員  
     * @return 確定した参加者のリスト  
     */  
    public abstract ConfirmedParticipants confirmParticipants(Participants participants,  
            EntryFrameCapacity entryFrameCapacity);  

}  

区分オブジェクト、というやつですね。先着順状態区分を表すenumで参加者確定の抽象メソッドを定義し、各区分値にて参加者の確定をどうやるか実装しています。

もうちょっとapplicationやservice、repositoryをしっかりやろうかな?と思ったんですが、Clrean Architecture読み始めたので、読み終わったらapplicationやservice、repositoryあたりをもうちょっと踏み込んで実装してみようかな?と考えています。

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

yodai_u_uの技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
or 外部アカウントではじめる
10秒で技術ブログが作れます!