私は貧乏人の [〜#〜] cqrs [〜#〜] を採用しています1 かなり長い間、1つのデータストアに粒度の細かいデータを格納できる柔軟性が大好きで、分析の大きな可能性を提供してビジネス価値を高め、パフォーマンスを向上させるために非正規化データを含む別のデータを必要とする場合もありました。
しかし、残念ながら最初からかなりの部分で、ビジネスロジックをこのタイプのアーキテクチャに配置する必要があるという問題に苦労してきました。
私が理解していることから、コマンドは意図を伝達するための手段であり、それ自体ではドメインとのつながりはありません。それらは基本的にデータ(ダム-必要に応じて)転送オブジェクトです。これは、異なるテクノロジー間でコマンドを簡単に転送できるようにするためです。正常に完了したイベントへの応答として同じことがイベントに適用されます。
典型的なDDDアプリケーションでは、ビジネスロジックはエンティティ、値オブジェクト、集約ルート内にあり、データと動作の両方が豊富です。ただし、コマンドはドメインオブジェクトではないため、データのドメイン表現に限定すべきではありません。これは、コマンドに過度の負担をかけるためです。
だから本当の質問は次のとおりです:ロジックはどこにあるのですか?
値の組み合わせに関するいくつかのルールを設定する非常に複雑な集合体を作成しようとするときに、私はこの闘争に最も頻繁に直面する傾向があることがわかりました。また、ドメインオブジェクトをモデリングするときは、オブジェクトがメソッドに到達したときにそれが有効な状態であることを知っている fail-fast パラダイムに従っています。
集約Car
が2つのコンポーネントを使用するとします。
Transmission
、Engine
。Transmission
とEngine
の両方の値オブジェクトはスーパータイプとして表され、対応するサブタイプ、Automatic
およびManual
送信、またはPetrol
およびElectric
エンジンそれぞれ。
このドメインでは、正常に作成されたTransmission
、Automatic
またはManual
のいずれか、またはEngine
のどちらのタイプも完全に正常に機能します。ただし、Car
集合体にはいくつかの新しいルールが導入されており、Transmission
およびEngine
オブジェクトが同じコンテキストで使用されている場合にのみ適用されます。つまり:
Electric
エンジンを使用する場合、許可されるトランスミッションタイプはAutomatic
のみです。Petrol
エンジンを使用している場合、どちらのタイプのTransmission
も使用できます。コマンドを作成するレベルでこのコンポーネントの組み合わせ違反をキャッチできますが、前に述べたように、コマンドにはドメインレイヤーに限定されるビジネスロジックが含まれるため、実行してはならないことがわかっています。
オプションの1つは、このビジネスロジック検証をコマンドバリデータ自体に移動することですが、これも正しくないようです。コマンドを分解し、ゲッターを使用して取得したコマンドのプロパティをチェックし、バリデーター内でそれらを比較して結果を検査しているように感じます。それは デメテルの法則 の違反のように私に叫びます。
上記の検証オプションは実行できないように見えるため破棄します。コマンドを使用して集計を作成する必要があるようです。しかし、このロジックはどこにあるべきでしょうか?具体的なコマンドを処理するコマンドハンドラ内にある必要がありますか?それともコマンドバリデータ内にあるべきですか(私はこのアプローチも好きではありません)?
私は現在コマンドを使用しており、責任のあるコマンドハンドラ内でそのコマンドから集約を作成しています。しかし、これを行うと、コマンドバリデーターがあったとしても、何も含まれなくなります。CreateCar
コマンドが存在する場合は、個別のケースで有効であることがわかっているコンポーネントが含まれますが、集計は異なると言う可能性があります。 。
異なる検証プロセスが混在する別のシナリオを想像してみましょう-CreateUser
コマンドを使用して新しいユーザーを作成します。
コマンドには、作成されるユーザーのId
とそのEmail
が含まれます。
システムには、ユーザーのメールアドレスに関する次のルールが記載されています。
この場合、一意の電子メールを持つことはビジネスルールですが、システム内の現在の電子メールのセット全体をメモリにロードし、コマンドで電子メールを確認する必要があるため、それをまとめて確認することはほとんど意味がありません。集計に対して(Eeeek!何か、何か、パフォーマンス。)そのため、このチェックをコマンドバリデーターに移動します。コマンドバリデーターは、依存関係としてUserRepository
を取り、リポジトリを使用して、コマンドに存在する電子メールを持つユーザーが既に存在するかどうかを確認します。
これに関しては、他の2つの電子メールルールをコマンドバリデーターに含めることは、突然意味があります。しかし、ルールは実際にUser
集計内に存在する必要があり、コマンドバリデーターは一意性のみをチェックし、検証が成功した場合はUser
集計を内に作成する必要があると感じていますCreateUserCommandHandler
して、保存するリポジトリに渡します。
リポジトリのsaveメソッドは集約を受け入れる可能性が高いため、このように感じます。集約が渡されると、すべての不変条件が満たされるようになります。ロジック(空でないなど)がコマンド検証自体にのみ存在する場合、別のプログラマーはこの検証を完全にスキップして、UserRepository
のsaveメソッドをUser
オブジェクトで直接呼び出すことができます。電子メールが長すぎる可能性があるため、致命的なデータベースエラーが発生します。
これらの複雑な検証と変換を個人的にどのように処理しますか?私は自分の解決策にほとんど満足していますが、自分のアイデアやアプローチが選択肢にかなり満足するのに完全に愚かではないことを確認する必要があるように感じます。私は完全に異なるアプローチに完全にオープンです。あなたが個人的に試してみてあなたのために非常にうまく機能したものがあるなら、私はあなたの解決策を見てみたいです。
1 PHP RESTfulシステムの作成を担当する開発者としての開発者)私のCQRSの解釈は、標準のasync-command-processingアプローチから少し逸脱しています。コマンドを同期的に処理する必要があるため、コマンドから結果を返すことがあります。
次の答えは、コマンドがアグリゲートに直接到着する cqrs.n によって促進されるCQRSスタイルのコンテキストにあります。このアーキテクチャスタイルでは、アプリケーションサービスは、集約を識別し、それをロードしてコマンドを送信し、集約を永続化するインフラストラクチャコンポーネント( CommandDispatcher )に置き換えられます(イベントの場合、一連のイベントとしてソーシングが使用されます)。
だから本当の質問は:ロジックはどこにあるのか
(検証)ロジックには複数の種類があります。一般的な考え方は、できるだけ早くロジックを実行することです-必要に応じてすばやく失敗します。したがって、状況は次のとおりです。
isValid
メソッドを使用する別のスタイルがありますが、実際にはコマンドのインスタンス化が成功した場合に誰かがこのメソッドを呼び出すことを覚えておかなければならないため、これは私には無意味です。command validators
、コマンドの検証を担当するクラス。複数の集計または外部ソースからの情報をチェックする必要がある場合、この種類の検証を使用します。これを使用して、ユーザー名の一意性を確認できます。 Command validators
には、リポジトリなどの依存関係を挿入できます。この検証は最終的には集計と整合することに注意してください(つまり、ユーザーが作成されると、その間に同じユーザー名の別のユーザーが作成される可能性があります)。また、アグリゲート内に存在するはずのロジックをここに配置しないでください!コマンドバリデーターは、イベントに基づいてコマンドを生成するSagas/Processマネージャーとは異なります。When a car uses Electric engine the only allowed transmission type is Automatic
をここでチェックする必要があります。リポジトリのsaveメソッドは集約を受け入れる可能性が高いため、このように感じます。集約が渡されると、すべての不変条件が満たされるようになります。ロジック(空でないなど)がコマンド検証自体にのみ存在する場合、別のプログラマーはこの検証を完全にスキップし、Userオブジェクトを使用してUserRepositoryのsaveメソッドを直接呼び出すことができます。これにより、致命的なデータベースエラーが発生する可能性があります。長すぎるかもしれません。
上記のテクニックを使用して、invalidコマンドを作成したり、集計内のロジックをバイパスしたりすることはできません。コマンドバリデーターはCommandDispatcher
によって自動的にロードされ+呼び出されるため、コマンドを集約に直接送信することはできません。コマンドを渡して集約のメソッドを呼び出すことはできますが、変更を永続化できなかったため、そうすることは無意味/無害です。
PHP RESTfulシステムの作成を担当する開発者としての開発者)私のCQRSの解釈は、コマンドを処理する必要があるためにコマンドから結果を返す場合があるなど、標準の非同期コマンド処理アプローチから少し逸脱しています。同期的に。
私もPHPプログラマーであり、コマンドハンドラー(handleSomeCommand
形式の集約メソッド)から何も返しません。ただし、頻繁にHTTP response
内のクライアント/ブラウザーへの情報。たとえば、新しく作成された集約ルートのIDまたは読み取りモデルからの何かなどですが、(reallynever)私の集約コマンドメソッドからのすべてコマンドが受け入れられた(そして処理された-同期について話しているPHP処理、そうですか?!))という単純な事実で十分です。
CQRSは高レベルのアーキテクチャーではない であるため、ブラウザーに何かを返します(それでも本ではCQRSを実行しています)。
コマンドバリデーターの機能の例:
DDDの基本的な前提の1つは、ドメインモデルが自身を検証することです。これは、ビジネスルールが適用されることを確認する責任者としてドメインを引き上げるため、重要な概念です。また、ドメインモデルを開発の焦点として保持します。
CQRSシステム(正しく指摘したとおり)は、独自の凝集メカニズムを実装する一般的なサブドメインを表す実装の詳細です。ビジネスルールに従って動作するために、モデルがCQRSインフラストラクチャの一部anyに依存することはありません。 DDDの目的は、システムのbehaviorをモデル化して、コアビジネスドメインの機能要件の有用な抽象化が得られるようにすることです。この動作の一部をモデルの外に移動すると、魅力的ではありますが、モデルの整合性とまとまりが低下します(そして、モデルの有用性が低下します)。
ChangeEmail
コマンドを含めるようにサンプルを拡張するだけで、ルールを複製する必要があるため、コマンドインフラストラクチャにビジネスロジックを必要としない理由を完全に説明できます。
ロジックをドメインに含める必要があることを確認できたので、「どこで」の問題に取り組みましょう。最初の2つのルールはUser
集計に簡単に適用できますが、最後のルールは少し微妙です。より深い洞察を得るためにさらに知識を駆使する必要があるもの。表面的には、このルールはUser
に適用されるように見えますが、実際には適用されません。電子メールの「一意性」は、Users
のコレクションに適用されます(一部の範囲による)。
ああ!このことを念頭に置くと、UserRepository
(Users
のメモリ内コレクション)がこの不変条件を適用するためのより良い候補である可能性があることが明らかになります。 「保存」メソッドは、チェックを含める最も妥当な場所です(UserEmailAlreadyExists
例外をスローできる場所)。あるいは、ドメインUserService
が新しいUsers
の作成とその属性の更新を担当するようにすることもできます。
フェイルファストは優れたアプローチですが、モデルの他の部分と一致する場所とタイミングでのみ実行できます。呼び出しがプロセスのよりどこかで失敗することがわかっている場合、開発者が失敗をキャッチしようとしてさらに処理する前に、アプリケーションサービスメソッド(またはコマンド)のパラメーターをチェックするのは非常に魅力的です。しかし、そうすることで、ビジネスルールが変更されたときにコードの複数の更新が必要になる可能性が高い方法で、知識が重複(および漏洩)します。