DDDアプリケーションサービスとREST APIの概念的な不一致


20

複雑なビジネスドメインと、REST API(厳密にはRESTではなく、リソース指向)をサポートする要件を持つアプリケーションを設計しようとしています。リソース指向の方法でドメインモデルを公開する方法を考えるのに苦労しています。

DDDでは、ドメインモデルのクライアントは、手続き型「アプリケーションサービス」層を通過して、エンティティおよびドメインサービスによって実装されるビジネス機能にアクセスする必要があります。たとえば、ユーザーエンティティを更新する2つのメソッドを持つアプリケーションサービスがあります。

userService.ChangeName(name);
userService.ChangeEmail(email);

このアプリケーションサービスのAPIは、状態ではなくコマンド(動詞、プロシージャ)を公開します。

ただし、同じアプリケーションにRESTful APIも提供する必要がある場合は、次のようなユーザーリソースモデルがあります。

{
name:"name",
email:"email@mail.com"
}

リソース指向のAPIは、コマンドではなく状態を公開します。これにより、次の懸念が生じます。

  • REST APIに対する各更新操作は、リソースモデルで更新されているプロパティに応じて、1つ以上のアプリケーションサービスプロシージャコールにマップできます。

  • 各更新操作は、REST APIクライアントにとってアトミックに見えますが、そのようには実装されていません。各アプリケーションサービス呼び出しは、個別のトランザクションとして設計されています。リソースモデルの1つのフィールドを更新すると、他のフィールドの検証ルールが変更される可能性があります。したがって、すべてのリソースモデルフィールドを一緒に検証して、潜在的なすべてのアプリケーションサービスコールが有効になるようにしてから、それらを作成する必要があります。一連のコマンドを一度に検証することは、一度に1つずつ実行することほど簡単ではありません。個々のコマンドが存在することすら知らないクライアントでそれをどのように行うのでしょうか?

  • アプリケーションサービスメソッドを異なる順序で呼び出すと効果が異なる場合がありますが、REST APIは違いがないように見えます(1つのリソース内)

もっと似たような問題を思いつくかもしれませんが、基本的にはすべて同じことが原因です。アプリケーションサービスを呼び出すたびに、システムの状態が変化します。有効な変更のルール、エンティティが次の変更を実行できるアクションのセット。リソース指向のAPIは、すべてをアトミック操作のように見せようとします。しかし、このギャップを越える複雑さはどこかに行かなければならず、それは巨大なようです。

さらに、UIがよりコマンド指向である場合(多くの場合そうです)、クライアント側でコマンドとリソースをマッピングし、API側に戻す必要があります。

質問:

  1. このすべての複雑さを(厚い)RESTからAppServiceへのマッピングレイヤーで処理するだけですか?
  2. または、DDD / RESTの理解に何か不足していますか?
  3. RESTは、特定の(かなり低い)複雑度でドメインモデルの機能を公開するために、単に実用的ではないでしょうか?

3
個人的にはRESTが必要だとは考えていません。しかし、それにDDDを押し込もすることが可能である: infoq.com/articles/rest-api-on-cqrsは programmers.stackexchange.com/questions/242884/... blog.42.nl/articles/rest-and-ddd-incompatible
デン

RESTクライアントをシステムのユーザーと考えてください。システムが実行するアクションをどのように実行するかについてはまったく気にしません。ユーザーが期待するよりも、RESTクライアントがドメイン上のすべての異なるアクションを認識することを期待することはないでしょう。あなたが言うように、このロジックはどこかに行かなければなりませんが、どのシステムでもどこかに行かなければなりません。RESTを使用していない場合は、クライアントに移動するだけです。それを正確に行わないことがRESTのポイントです。クライアントは、状態を更新したいということだけを知っている必要があり、それについてどう考えればよいのでしょうか。
コーマックマルホール

2
@astr簡単な答えは、リソースはモデルではないため、リソース処理コードの設計がモデルの設計に影響を与えるべきではないということです。リソースは、モデルが内部にあるシステムの外向きの側面です。リソースについては、UIと同じように考えてください。ユーザーがUIの1つのボタンをクリックすると、モデルでさまざまなことが起こります。リソースに似ています。クライアントがリソース(単一のPUTステートメント)を更新すると、モデルで数百万の異なることが発生する可能性があります。モデルをリソースに密接に結合することはアンチパターンです。
コーマックマルホール

1
これは、ドメイン内のアクションをREST状態の変化の副作用として扱い、ドメインとWebを分離する(ジューシービットの場合は25分まで早送り)yow.eventer.com/events/1004/talks/1047
コーマックマルホール

1
また、「ロボット/ステートマシンとしてのユーザー」全体についてもわかりません。私たちはユーザーインターフェイスをそれよりもはるかに自然なものにしようと努力すべきだと思います
...-guillaume31

回答:


10

私は同じ問題を抱えており、RESTリソースを異なる方法でモデリングすることでそれを「解決」しました。例えば:

/users/1  (contains basic user attributes) 
/users/1/email 
/users/1/activation 
/users/1/address

したがって、基本的には、大きくて複雑なリソースをいくつかの小さなリソースに分割しました。これらのそれぞれには、一緒に処理されると予想される元のリソースの属性の多少まとまりのあるグループが含まれています。

これらのリソースに対する各操作はアトミックです。いくつかのサービスメソッドを使用して実装することもできます-少なくともSpring / Java EEでは、元々独自のトランザクションを持つことを意図したいくつかのメソッドから大きなトランザクションを作成することは問題ありません(REQUIREDトランザクションを使用)伝搬)。多くの場合、この特別なリソースに対して追加の検証を行う必要がありますが、属性が凝集している(と思われる)ので、まだ非常に管理しやすいです。

これはHATEOASアプローチにも適しています。よりきめの細かいリソースは、リソースで簡単に表すことができないため、クライアントとサーバーの両方でこのロジックを持つのではなく、それらでできることに関する情報をより多く伝えるためです。

もちろん、完璧ではありません-これらのリソース(特にデータ指向のUI)を念頭に置いてUIをモデル化しないと、いくつかの問題が発生する可能性があります。たとえば、UIは特定のリソース(およびそのサブリソース)のすべての属性の大きなフォームを提示し、それらをすべて編集し、一度に保存します-クライアントが複数のリソース操作を呼び出す必要がある場合でも、原子性の錯覚を作成します(それ自体はアトミックですが、シーケンス全体はアトミックではありません)。

また、このリソースの分割は、簡単または明白ではない場合があります。私は主に、複雑さを管理するために複雑な動作/ライフサイクルを持つリソースでこれを行います。


それも私が考えてきたことです-書き込み操作にとってより便利なので、より詳細なリソース表現を作成します。リソースが非常に細かくなった場合、どのようにリソースのクエリを処理しますか?読み取り専用の非正規化表現も作成しますか?
astreltsov

1
いいえ、読み取り専用の非正規化表現はありません。私はjsonapi.org標準を使用し、特定のリソースの応答に関連リソースを含めるメカニズムを備えています。基本的に、「ID 1のユーザーを指定し、そのサブリソースの電子メールとアクティベーションも含めてください」と言います。これは、サブリソースに対する余分なREST呼び出しを取り除くのに役立ち、優れたJSON APIクライアントライブラリを使用する場合、サブリソースを扱うクライアントの複雑さに影響しません。
qbd

したがって、サーバー上の単一のGET要求は、1つ以上の実際のクエリ(含まれるサブリソースの数に応じて)に変換され、それらは単一のリソースオブジェクトに結合されますか?
astreltsov

複数レベルのネストが必要な場合はどうなりますか?
astreltsov

はい、リレーショナルデータベースでは、これはおそらく複数のクエリに変換されます。任意のネストはJSON APIでサポートされています。これについては、jsonapi.org
format

0

ここでの重要な問題は、REST呼び出しが行われたときにビジネスロジックが透過的に呼び出される方法です。これは、RESTによって直接対処されない問題です。

JPAなどの永続性プロバイダー上に独自のデータ管理レイヤーを作成することで、これを解決しました。メタモデルとカスタムアノテーションを使用して、エンティティの状態が変化したときに適切なビジネスロジックを呼び出すことができます。これにより、エンティティの状態の変化に関係なく、ビジネスロジックが呼び出されます。アーキテクチャをDRYに保ち、ビジネスロジックを1か所に保持します。

上記の例を使用すると、RESTを使用して名前フィールドが変更されたときにvalidateNameというビジネスロジックメソッドを呼び出すことができます。

class User { 
      String name;
      String email;

      /**
       * This method will be transparently invoked when the value of name is changed
       * by REST.
       * The XorUpdate annotation becomes effective for PUT/POST actions
       */
      @XorPostChange
      public void validateName() {
        if(name == null) {
          throw new IllegalStateException("Name cannot be set as null");
        }
      }
    }

このようなツールを自由に使用できれば、ビジネスロジックメソッドに適切に注釈を付けるだけで十分です。


0

リソース指向の方法でドメインモデルを公開する方法を考えるのに苦労しています。

リソース指向の方法でドメインモデルを公開するべきではありません。リソース指向の方法でアプリケーションを公開する必要があります。

UIがよりコマンド指向である場合(これがよくあるケースです)、クライアント側でコマンドとリソースをマッピングし、API側にマッピングする必要があります。

まったくない-ドメインモデルとインターフェイスするアプリケーションリソースにコマンドを送信します。

REST APIに対する各更新操作は、リソースモデルで更新されているプロパティに応じて、1つ以上のアプリケーションサービスプロシージャコールにマップできます。

はい。ただし、これを綴るために少し異なる方法があります。REST APIに対する各更新操作は、コマンドを1つ以上の集約にディスパッチするプロセスにマップします。

各更新操作は、REST APIクライアントにとってアトミックに見えますが、そのようには実装されていません。各アプリケーションサービス呼び出しは、個別のトランザクションとして設計されています。リソースモデルの1つのフィールドを更新すると、他のフィールドの検証ルールが変更される可能性があります。したがって、すべてのリソースモデルフィールドを一緒に検証して、潜在的なすべてのアプリケーションサービスコールが有効になるようにしてから、それらを作成する必要があります。一連のコマンドを一度に検証することは、一度に1つずつ実行することほど簡単ではありません。個々のコマンドが存在することすら知らないクライアントでそれをどのように行うのでしょうか?

ここで間違った尾を追いかけています。

想像してみてください。RESTを完全に削除してください。代わりに、このアプリケーションのデスクトップインターフェイスを作成していると想像してください。さらに、非常に優れた設計要件があり、タスクベースのUIを実装していることを想像してみましょう。そのため、ユーザーは、作業中のタスクに合わせて完全に調整された最小限のインターフェイスを取得できます。ユーザーはいくつかの入力を指定し、「動詞」を押します。ボタン。

今、何が起きた?ユーザーの観点から見ると、これは実行すべき単一のアトミックタスクです。domainModelの観点から見ると、それは集約によって実行されるいくつかのコマンドであり、各コマンドは個別のトランザクションで実行されます。それらは完全に互換性がありません!ギャップを埋めるために中間に何かが必要です!

何かが「アプリケーション」です。

幸いなことに、アプリケーションはDTOを受け取り、そのオブジェクトを解析して理解できるメッセージを取得し、メッセージ内のデータを使用して1つ以上の集約用の整形式コマンドを作成します。アプリケーションは、アグリゲートにディスパッチする各コマンドが適切に形成されていることを確認し(作業中の腐敗防止レイヤー)、アグリゲートをロードし、トランザクションが正常に完了した場合にアグリゲートを保存します。集約は、現在の状態を考慮して、コマンドが有効かどうかを判断します。

考えられる結果-コマンドはすべて正常に実行されます-腐敗防止レイヤーはメッセージを拒否します-一部のコマンドは正常に実行されますが、その後、アグリゲートの1つがエラーを出し、軽減することができます。

ここで、そのアプリケーションをビルドしたと想像してください。RESTfulな方法でどのようにやり取りしますか?

  1. クライアントは、ハイパーメディアコントロールを含む、現在の状態(つまり、タスクベースのUI)のハイパーメディア記述で始まります。
  2. クライアントは、タスクの表現(つまり、DTO)をリソースにディスパッチします。
  3. リソースは、着信HTTP要求を解析し、表現を取得して、アプリケーションに渡します。
  4. アプリケーションはタスクを実行します。リソースの観点から見ると、これは次のいずれかの結果を持つブラックボックスです
    • アプリケーションはすべての集約を正常に更新しました:リソースはクライアントに成功を報告し、それを新しいアプリケーション状態に誘導します
    • 腐敗防止層はメッセージを拒否します。リソースは4xxエラーをクライアントに報告し(おそらく不正な要求)、発生した問題の説明を伝えます。
    • アプリケーションはいくつかの集計を更新します。リソースはコマンドが受け入れられたことをクライアントに報告し、コマンドの進行状況を表すリソースにクライアントを誘導します。

受け入れられるのは、アプリケーションがクライアントに応答するまでメッセージの処理を延期する場合の通常の警戒です。これは一般に非同期コマンドを受け入れるときに使用されます。しかし、アトミックであると想定される操作に緩和が必要なこの場合にもうまく機能します。

このイディオムでは、リソースはタスク自体を表します。適切な表現をタスクリソースにポストすることでタスクの新しいインスタンスを開始し、そのリソースはアプリケーションと連動し、次のアプリケーション状態に進みます。

では、あなたが複数のコマンドを調整しているほとんどすべての時間は、あなたが(ビジネス・プロセス別名、佐賀別名)プロセスの観点で考えることにしたいです。

読み取りモデルにも同様の概念的な不一致があります。繰り返しになりますが、タスクベースのインターフェイスを検討してください。タスクが複数の集約を変更する必要がある場合、タスクを準備するためのUIには、多くの集約からのデータが含まれている可能性があります。リソーススキームが集約で1:1の場合、それを調整するのは難しいでしょう。代わりに、上記のように「開始タスク」関係をタスクエンドポイントにマップするハイパーメディアコントロールとともに、いくつかの集計からデータの表現を返すリソースを提供します。

関連項目:Jim WebberによるREST in Practice


ユースケースに従ってドメインと対話するAPIを設計している場合.. Sagasがまったく必要ないような方法で設計しないのはなぜですか?何かが足りないかもしれませんが、あなたの応答を読むことで、RESTはDDDとの相性が良くなく、リモートプロシージャ(RPC)を使用する方が良いと確信しています。DDDは動作中心で、RESTはhttp動詞中心です。画像からRESTを削除して、APIの動作(コマンド)を公開しないのはなぜですか?結局、おそらくユースケースシナリオを満たすように設計されており、問題はトランザクションです。UIを所有している場合、RESTの利点は何ですか?
iberodev
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.