CQRSコマンドをどの程度正確に検証し、ドメインオブジェクトに変換する必要がありますか?


22

あるデータストアにきめ細かなデータを持ち、分析の大きな可能性を提供し、ビジネス価値を向上させ、必要に応じてパフォーマンスを向上させるために非正規化データを含む読み取りを必要とする柔軟性を備えているため、貧乏人CQRS 1をかなり長い間適応させてきました。

しかし、残念ながら最初から、このタイプのアーキテクチャにビジネスロジックを正確に配置する必要があるという問題に苦労してきました。

私が理解していることから、コマンドは意図を伝える手段であり、ドメイン自体とは関係がありません。これらは基本的にデータ(ダム-必要に応じて)転送オブジェクトです。これは、異なるテクノロジー間でコマンドを簡単に転送できるようにするためです。同じことが、正常に完了したイベントへの応答としてイベントに適用されます。

典型的なDDDアプリケーションでは、ビジネスロジックはエンティティ、値オブジェクト、集約ルート内に存在し、データだけでなく動作も豊富です。ただし、コマンドはドメインオブジェクトではないため、データのドメイン表現に限定されるべきではありません。これは、コマンドに過度の負担をかけるためです。

本当の質問は、ロジックはどこにあるのでしょうか?

値の組み合わせについていくつかのルールを設定する非常に複雑な集合体を構築しようとすると、私はこの闘争に最も頻繁に直面することがわかりました。また、ドメインオブジェクトをモデル化するときは、フェイルファーストパラダイムに従い、オブジェクトがいつメソッドに到達するかを知ることが有効な状態にあることを好みます。

集約Carが2つのコンポーネントを使用するとします。

  • Transmission
  • Engine

両方TransmissionEngine値オブジェクトは、スーパータイプとして表され、サブタイプ、記載しているAutomaticManualトランスミッション、またはPetrolElectricそれぞれエンジン。

このドメインでは、それ自体で生活TransmissionするAutomaticManual、成功するか、それとも、またはいずれかのタイプEngineが完全に作成されます。しかし、Car集約は該当する場合にのみ、いくつかの新しいルールを紹介Transmissionし、Engineオブジェクトが同じコンテキストで使用されています。すなわち:

  • 車がElectricエンジンを使用する場合、許可されるトランスミッションタイプはのみですAutomatic
  • 車がPetrolエンジンを使用するとき、それはどちらのタイプも持つかもしれませんTransmission

コマンドを作成するレベルでこのコンポーネントの組み合わせの違反をキャッチできましたが、前に述べたように、コマンドにはドメインレイヤーに限定されるべきビジネスロジックが含まれるため、実行すべきではないことを理解しているからです。

オプションの1つは、このビジネスロジック検証をコマンドバリデータ自体に移動することですが、これも正しくないようです。コマンドを分解し、ゲッターを使用して取得したプロパティを確認し、バリデーター内でそれらを比較して結果を検査しているように感じます。それは私にとってデメテル法則違反のような叫びです。

上記の検証オプションは実行可能とは思えないため、破棄することは、コマンドを使用してそれから集計を構築する必要があるようです。しかし、このロジックはどこにあるべきでしょうか?具体的なコマンドを処理するコマンドハンドラー内にあるべきですか?それとも、おそらくコマンドバリデーター内にあるべきでしょう(このアプローチも好きではありません)。

現在、コマンドを使用しており、責任あるコマンドハンドラー内でコマンドから集計を作成しています。しかし、これを行うと、CreateCarコマンドが存在する場合、コマンドバリデータがまったく含まれないため、コマンドバリデータがあれば、別のケースで有効であることがわかっているコンポーネントが含まれますが、集計は異なると言う場合があります。


CreateUserコマンドを使用して新しいユーザーを作成する、さまざまな検証プロセスを組み合わせたさまざまなシナリオを想像してみましょう。

コマンドには、Id作成されたユーザーとそのが含まれますEmail

システムは、ユーザーの電子メールアドレスについて次のルールを示します。

  • 一意である必要があり、
  • 空にしないでください、
  • 最大100文字(db列の最大長)でなければなりません。

この場合、一意の電子メールを持つことはビジネスルールですが、システム内の現在の電子メールのセット全体をメモリにロードし、コマンドで電子メールをチェックする必要があるため、集約でチェックすることはほとんど意味がありません集合体に対して(Eeeek!何か、何か、パフォーマンス。)。そのため、このチェックをコマンドバリデータに移動します。コマンドバリデータはUserRepository、依存関係として受け取り、リポジトリを使用して、コマンドに電子メールが存在するユーザーが既に存在するかどうかをチェックします。

これに関しては、他の2つの電子メールルールをコマンドバリデーターに入れることは突然意味があります。しかし、ルールはUser集約内に実際に存在し、コマンドバリデーターは一意性のみをチェックし、検証が成功した場合は、User集約を作成CreateUserCommandHandlerしてリポジトリに渡し、保存する必要があると感じています。

リポジトリのsaveメソッドは、集約が渡されるとすべての不変条件が満たされることを保証する集約を受け入れる可能性が高いため、このように感じます。ロジック(空でないなど)がコマンド検証自体にのみ存在する場合、別のプログラマーはこの検証を完全にスキップUserRepositoryして、Userオブジェクトでsaveメソッドを直接呼び出すことができ、致命的なデータベースエラーにつながる可能性があります長すぎました。

これらの複雑な検証と変換をどのように個人的に処理しますか?私はほとんど自分のソリューションに満足していますが、選択肢にかなり満足するためには、私のアイデアとアプローチが完全に愚かではないことを断言する必要があると感じています。私は完全に異なるアプローチに完全にオープンです。個人的に何か試してみて、あなたのために非常にうまくいったことがあるなら、私はあなたの解決策を見たいと思います。


1 RESTfulシステムの作成を担当するPHP開発者として働いている私のCQRSの解釈は、コマンドを同期的に処理する必要があるためにコマンドから結果を返すことがあるなど、標準の非同期コマンド処理アプローチとは少し異なります。


サンプルコードが必要だと思います。コマンドオブジェクトはどのように見え、どこで作成しますか?
ユアン

@Ewan今日または明日の後半にコードサンプルを追加します。数分で旅行に出発します。
アンディ

PHPプログラマーであるため、CQRS + ESの実装をご覧になることをお勧めします。github.com/ xprt64
Constantin Galbenu

@ConstantinGALBENU CQRSのGreg Youngの解釈が正しいと考えられる場合(おそらくそうすべきです)、CQRSの理解が間違っています-または少なくともPHPの実装は正しいです。コマンドは集約によって直接処理されません。コマンドはコマンドハンドラーによって処理されます。コマンドハンドラーは、集約の変更を生成し、状態のレプリケーションに使用されるイベントを生成します。
アンディ

私たちの解釈が違うとは思いません。単にDDDを掘り下げて(総計の戦術レベルで)または目を大きく開くだけです。CQRSの実装には、少なくとも2つのスタイルがあります。私はそれらの1つを使用します。私の実装は、アクターモデルにより似ており、アプリケーション層を非常に薄くしています。これは常に良いことです。これらのアプリサービス内には多くのコードの重複があることに気づき、それらをに置き換えることにしましたCommandDispatcher
コンスタンタンガルベヌ

回答:


22

次の答えは、コマンドが集約に直接届くcqrs.nuによって促進された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メソッドを直接呼び出すことができます。長すぎたかもしれません。

上記の手法を使用すると、無効なコマンドを作成したり、集計内のロジックをバイパスしたりすることはできません。コマンドバリデータはによって自動的にロードされ、呼び出されるCommandDispatcherため、誰もコマンドを集約に直接送信できません。コマンドを渡す集約のメソッドを呼び出すことはできますが、変更を永続化できなかったので、そうすることは無意味で無害です。

RESTfulシステムの作成を担当するPHP開発者として働いているため、CQRSの解釈は、コマンドを同期処理する必要があるためにコマンドから結果を返すことがあるなど、標準の非同期コマンド処理アプローチから少し外れています。

私はPHPプログラマーでもあり、コマンドハンドラー(フォームの集約メソッドhandleSomeCommand)からは何も返しません。ただし、頻繁に、HTTP responseたとえば、新しく作成された集約ルートのIDや読み取りモデルからの何かなどの情報をクライアント/ブラウザーに返しますが、集約コマンドメソッドからは何も返しません(実際に決して返しません)。コマンドが受け入れられた(そして処理された-同期PHP処理について話しているのですか?!)という単純な事実で十分です。

CQRSは高レベルのアーキテクチャではないため、ブラウザに何かを返します(そして、本でCQRSを実行しています)。

コマンド検証機能の動作例:

集計に向かう途中のコマンド検証ツールを通るコマンドのパス


検証戦略に関して、ポイント2は、ロジックが頻繁に複製される可能性が高い場所として私に飛び出します。確かに、ユーザー集合体が空ではなく整形式の電子メールを検証することを望むでしょうか?これは、ChangeEmailコマンドを導入すると明らかになります。
キングサイドスライド

@ king-side-slideは、自分EmailAddress自身を検証する値オブジェクトを持っている場合ではありません。
コンスタンティンガルベヌ

それは完全に正しいです。EmailAddress重複を減らすためにをカプセル化できます。さらに重要なことは、そうすることで、ロジックをコマンドからドメインに移動することにもなります。これはあまりにも遠いことができることに注意する価値があります。多くの場合、類似の知識(値オブジェクト)には、その使用者に応じて異なる検証要件があります。EmailAddressこの値の概念全体にグローバルな検証要件があるため、便利な例です。
キングサイドスライド

同様に、「コマンドバリデータ」のアイデアは不要なようです。目標は、無効なコマンドが作成およびディスパッチされるのを防ぐことではありません。目標は、それらの実行を防ぐことです。たとえば、必要なデータをURLで渡すことができます。無効な場合、システムは要求を拒否します。コマンドは引き続き作成され、ディスパッチされます。コマンドが検証のために複数の集計(つまり、電子メールの一意性を確認するためのユーザーの集合)を必要とする場合、ドメインサービスがより適しています。「xバリデータ」などのオブジェクトは、多くの場合、データが動作から分離されている貧血モデルの兆候です。
キングサイドスライド

1
@ king-side-slide具体的な例はUserCanPlaceOrdersOnlyIfHeIsNotLockedValidatorです。これはOrdersの別のドメインであるため、OrderAggregate自体で検証することはできません。
コンスタンティンガルベヌ

6

DDDの基本的な前提の1つは、ドメインモデルが自身を検証することです。これは、ビジネスルールが確実に実施されるようにする責任者としてドメインを昇格させるため、重要な概念です。また、開発の焦点としてドメインモデルを保持します。

CQRSシステム(正確に指摘すると)は、独自の凝集メカニズムを実装する汎用サブドメインを表す実装の詳細です。あなたのモデルは決してに依存すべきすべてのビジネスルールに従って動作するようにCQRSインフラストラクチャの一部。DDDの目標は、結果がコアビジネスドメインの機能要件の有用な抽象化となるように、システムの動作をモデル化することです。この振る舞いの一部をモデルの外に移動すると、魅力的ですが、モデルの整合性と凝集度が低下します(そして、モデルの有用性が低下します)。

例を拡張してChangeEmailコマンドを含めるだけで、ルールを複製する必要があるため、コマンドインフラストラクチャにビジネスロジックが必要ない理由を完全に説明できます。

  • メールを空にすることはできません
  • メールは100文字を超えることはできません
  • メールは一意でなければなりません

ロジックがドメイン内にある必要があることを確認できたので、「どこ」の問題に取り組みましょう。最初の2つのルールはUser集計に簡単に適用できますが、最後のルールはもう少し微妙です。より深い洞察を得るために、さらなる知識を必要とするもの。表面的には、このルールはa Userに適用されるように見えるかもしれませんが、実際には適用されません。電子メールの「一意性」は、Users(ある範囲による)のコレクションに適用されます。

あぁ!それを念頭に置いて、あなたのUserRepository(あなたのインメモリコレクションUsers)がこの不変条件を実施するためのより良い候補であるかもしれないことが十分に明らかになります。「保存」メソッドは、おそらくチェックを含めるための最も妥当な場所です(UserEmailAlreadyExists例外をスローできる場所)。または、ドメインUserServiceを作成しUsersて属性を更新することもできます。

フェイルファーストは優れたアプローチですが、モデルの残りの部分に適合する場所でのみ実行できます。(開発者が)呼び出しがプロセスのどこか深いところで失敗することがわかっている場合、失敗をキャッチするために、さらに処理する前にアプリケーションサービスメソッド(またはコマンド)のパラメーターをチェックすることは非常に魅力的です。しかし、そうすることで、ビジネスルールが変更されたときにコードを複数回更新する必要があるような方法で知識を複製(および漏洩)することになります。


2
これに同意する。(CQRSを使用せずに)今まで読んだことから、不変条件を保護するために、検証は常にドメインモデルで行う必要があることがわかりました。今、私はCQRSを読んでいます、それはCommandオブジェクトに検証を置くように言っています。これは直感に反するようです。たとえば、コマンドの代わりにドメインモデルに検証が行われるGitHubの例を知っていますか?+1。
w0051977
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.