しゃべぶろ

気になった技術の備忘録を残します。

【ドメイン駆動設計】サンプル(iddd_agilepm)を読解く その2

概要

前回はパッケージ構成について触れたので、
今回はアプリケーションサービスについて触れてみる。
hghyk023.hatenablog.jp

アプリケーションサービス

IDDD本の中で以下のように定義されている。

アプリケーションサービスは薄く保ち、モデル上でのタスクの調整にだけ使うようにすること。

アプリケーションサービスは、ドメインモデルやドメインサービスのクライアントとなり、
両者で表現すべきではない、アプリケーション要件を実現する時に使うべきである。
例えば、認証機能やメール送信機能などはアプリケーションサービスの責務となる。

iddd_agilepmでのアプリケーションサービスの使い方を覗いてみる。

applicationパッケージ

applicationパッケージ配下はこんな感じ。

application
├── ApplicationServiceLifeCycle.java
├── notification
│   └── NotificationApplicationService.java
├── process
│   └── ProcessApplicationService.java
├── product
│   ├── InitiateDiscussionCommand.java
│   ├── NewProductCommand.java
│   ├── ProductApplicationService.java
│   ├── RequestProductDiscussionCommand.java
│   ├── RetryProductDiscussionRequestCommand.java
│   ├── StartDiscussionInitiationCommand.java
│   ├── TimeOutProductDiscussionRequestCommand.java
│   └── backlogitem
│       └── BacklogItemApplicationService.java
├── sprint
│   ├── CommitBacklogItemToSprintCommand.java
│   └── SprintApplicationService.java
└── team
    ├── ChangeTeamMemberEmailAddressCommand.java
    ├── ChangeTeamMemberNameCommand.java
    ├── DisableMemberCommand.java
    ├── DisableProductOwnerCommand.java
    ├── DisableTeamMemberCommand.java
    ├── EnableMemberCommand.java
    ├── EnableProductOwnerCommand.java
    ├── EnableTeamMemberCommand.java
    └── TeamApplicationService.java
  • ApplicationServiceLifeCycleというクラスが存在する。
  • 1 つのServiceに対して複数のCommandクラスが関連している。

ApplicationServiceLifeCycle

  • 各サービスの処理開始/終了時に呼び出されている。
  • LevelDBとSlothMQを使ってイベントストアにドメインイベントを登録している。
  • 確かにこれはapplicationパッケージ配下に置くものだね。

ServiceとCommandクラス

例として、ProductApplicationServiceを取り上げる。
以下はinitiateDiscussionメソッドを抜粋

public void initiateDiscussion(InitiateDiscussionCommand aCommand) {
    ApplicationServiceLifeCycle.begin();

    try {
        Product product =
                this.productRepository()
                    .productOfId(
                            new TenantId(aCommand.getTenantId()),
                            new ProductId(aCommand.getProductId()));

        if (product == null) {
            throw new IllegalStateException(
                    "Unknown product of tenant id: "
                    + aCommand.getTenantId()
                    + " and product id: "
                    + aCommand.getProductId());
        }

        product.initiateDiscussion(new DiscussionDescriptor(aCommand.getDiscussionId()));

        this.productRepository().save(product);

        ProcessId processId = ProcessId.existingProcessId(product.discussionInitiationId());

        TimeConstrainedProcessTracker tracker =
                this.processTrackerRepository()
                    .trackerOfProcessId(aCommand.getTenantId(), processId);

        tracker.completed();

        this.processTrackerRepository().save(tracker);

        ApplicationServiceLifeCycle.success();

    } catch (RuntimeException e) {
        ApplicationServiceLifeCycle.fail(e);
    }
}
  • まず、リポジトリを使って集約ルートのProductオブジェクト取得する。
    • リポジトリの引数としてVOを渡している、newして渡しているのが結構斬新(自分だけかも)
    • VOは使い捨てることが出来るので、こういう使い方が出来るんだろうね。
    • Productが無かったら、Exceptionをthrowする。
  • initiateDiscussionメソッドで自身の状態を変化させ、リポジトリを使って永続化する。
  • ProcessId、TimeConstrainedProcessTrackerなんだこれ...w
    • ProcessIdは別プロジェクトのiddd_commonでドメインモデルとして定義されている。
    • DBにプロセスID単位で書き込んで、処理の状態を確認出来るようにしているのかな??


上記メソッド以外も見てみる。

public String newProduct(NewProductCommand aCommand) {...}
public String newProductWithDiscussion(NewProductCommand aCommand) {...}
public void requestProductDiscussion(RequestProductDiscussionCommand aCommand) {...}
private void sendEmailForTimedOutProcess(Product aProduct) {...}
private void requestProductDiscussionFor(Product aProduct) {...}
  • 恐らくユースケース毎にメソッドが作成されており、ユビキタス言語を反映したメソッド名になっている。
  • publicメソッドは引数にCommandクラス、privateメソッドがCommadクラスを使用していない。


あとCommandクラスも1個取り上げてみる。

public class NewProductCommand {

    private String tenantId;
    private String productOwnerId;
    private String name;
    private String description;

    public NewProductCommand(String tenantId, String productOwnerId, String name, String description) {
        super();

        this.tenantId = tenantId;
        this.productOwnerId = productOwnerId;
        this.name = name;
        this.description = description;
    }
// getter/setter省略
}
  • CommandクラスはPOJOであり、getter/setterしかない。
    • フィールドは標準クラスで構成されており、VOは使用されていない。
    • サービスのクラアイントにドメインの情報を漏らさないようにしている??

まとめ

今回の例だと、アプリケーションサービスはドメインモデルの永続化やイベントストアへの登録を担当しており、ドメインロジックは記述されていなかった。
次はドメインモデルを読解いて、アプリケーションサービスとの違いを理解したい。