コマンドから結果データが必要な場合、コマンドクエリ分離(CQS)をどのように適用しますか?

StackOverflow https://stackoverflow.com/questions/3688068

  •  02-10-2019
  •  | 
  •  

質問

ウィキペディアの定義で コマンドクエリ分離, 、それは述べられています

より正式には、メソッドは参照的に透明であるため、副作用がない場合にのみ値を返す必要があります。

コマンドを発行している場合、この定義により関数がデータを返すことができないため、そのコマンドが成功したかどうかをどのように判断または報告する必要がありますか?

例えば:

string result = _storeService.PurchaseItem(buyer, item);

この呼び出しにはコマンドとクエリの両方が含まれていますが、クエリ部分はコマンドの結果です。コマンドパターンを使用してこれをリファクタリングできると思います。

PurchaseOrder order = CreateNewOrder(buyer, item);
_storeService.PerformPurchase(order);
string result = order.Result;

しかし、これはコードのサイズと複雑さを増しているようです。

操作の結果が必要な場合、誰かが私にコマンドクエリ分離を達成するためのより良い方法を与えることができますか?

ここに何かが足りませんか?

ありがとう!

注:マーティン・ファウラーは、CQSの限界について言うことを持っています CommandQuerySeparation:

マイヤーはコマンドクエリの分離を絶対に使用するのが好きですが、例外があります。スタックをポップすることは、状態を変更する修飾子の良い例です。 Meyerは、この方法を避けることはできないと正しく言っていますが、それは有用なイディオムです。だから私はできる限りこの原則に従うことを好みますが、私はそれを壊してポップを得る準備ができています。

彼の見解から、いくつかの小さな単純な例外を除いて、コマンド/クエリの分離に向けてリファクタリングすることはほとんど常に価値があります。

役に立ちましたか?

解決

この質問は古いですが、まだ満足のいく答えを受け取っていないので、ほぼ1年前のコメントについて少し詳しく説明します。

イベント駆動型のアーキテクチャを使用すると、明確なコマンド/クエリの分離を達成するだけでなく、新しいアーキテクチャの選択肢を開き、通常は非同期プログラミングモデルに適合するため(アーキテクチャを拡大する必要がある場合に便利です)、非常に理にかなっています。多くの場合、ソリューションはドメインのモデルを異なる方法でモデル化することにある可能性があります。

それでは、購入の例を取りましょう。 StoreService.ProcessPurchase 購入を処理するのに適したコマンドになります。これはaを生成します PurchaseReceipt. 。これは、領収書を戻すのではなく、より良い方法です Order.Result. 。物事を非常にシンプルに保つために、領収書をコマンドから返品し、ここでCQRSに違反することができます。よりクリーンな分離が必要な場合、コマンドは ReceiptGenerated 購読できるイベント。

ドメインについて考えると、これは実際にはより良いモデルになる可能性があります。レジ係でチェックアウトするときは、このプロセスに従います。領収書が生成される前に、クレジットカードチェックが期限切れになる場合があります。これには時間がかかる可能性があります。同期シナリオでは、キャッシャーで待つことができ、他に何もできません。

他のヒント

CQSとCQRSの間には多くの混乱が見られます(Mark Rogersも1つの答えで気づいたように)。

CQRSはDDDの建築的アプローチであり、クエリの場合、すべてのエンティティとバリュータイプを備えた集計ルーツから完全な吹き飛ばされたオブジェクトグラフを構築するのではなく、リストに表示する軽量ビューオブジェクトを構築します。

CQSは、アプリケーションのどの部分でもコードレベルに関する優れたプログラミングの原則です。ドメイン領域だけではありません。原則は、DDD(およびCQR)よりもはるかに長く存在します。アプリケーションの状態を変更するコマンドを台無しにしないでください。データを返すだけで、状態を変更せずにいつでも呼び出すことができるクエリを使用します。 Delphiとの昔、ランケージは機能と手順の違いを示しました。 「functionprocedures」をコーディングするのは悪い慣行と考えられていました。

質問に答えるために、尋ねられた:コマンドを実行して結果を取り戻す方法を考えることができます。たとえば、void executeメソッドとreadonlyコマンド結果プロパティを備えたコマンドオブジェクト(コマンドパターン)を提供します。

しかし、CQSを遵守する主な理由は何ですか?実装の詳細を調べる必要なく、コードを読み取り可能で再利用可能に保ちます。コードは、予期しない副作用を引き起こさないように信頼できる必要があります。したがって、コマンドが結果を返したい場合、および関数名または戻りオブジェクトがコマンドの結果を持つコマンドであることを明確に示している場合、CQSルールの例外を受け入れます。物事をより複雑にする必要はありません。ここでは、Martin Fowler(上記)に同意します。

ちなみに、この規則に厳密に従うことは、流fluent API原理全体を破壊しませんか?

私は他の人が与えたイベント主導のアーキテクチャの提案が好きですが、私はただ別の視点を投げ込みたいだけです。たぶん、なぜあなたが実際にあなたのコマンドからデータを返しているのかを見る必要があるかもしれません。実際に結果が必要ですか、それとも失敗した場合に例外を投げることで逃げることができますか?

私はこれを普遍的な解決策と言っているのではなく、「応答を送信する」モデルではなく、より強力な「失敗の例外」に切り替えることで、分離が実際に自分のコードで機能するようにするのに役立ちました。もちろん、より多くの例外ハンドラーを書く必要があるので、それはトレードオフです...しかし、それは少なくとも別の角度です。

質問は;コマンドの結果が必要な場合、どのようにCQを適用しますか?

答えは次のとおりです。コマンドを実行して結果を取り戻す場合は、CQSを使用していません。

しかし、黒と白の独断的な純度は宇宙の死になる可能性があります。常にエッジケースと灰色の領域があります。問題は、CQの形式であるが、もはや純粋なCQではないパターンを作成し始めることです。

モナドは可能性です。コマンドがvoidを返す代わりに、Monadを返すことができます。 「void」モナドは次のようになるかもしれません:

public class Monad {
    private Monad() { Success = true; }
    private Monad(Exception ex) {
        IsExceptionState = true;
        Exception = ex;
    }

    public static Monad Success() => new Monad();
    public static Monad Failure(Exception ex) => new Monad(ex);

    public bool Success { get; private set; }
    public bool IsExceptionState { get; private set; }
    public Exception Exception { get; private set; }
}

これで、次のような「コマンド」メソッドを使用できます。

public Monad CreateNewOrder(CustomerEntity buyer, ProductEntity item, Guid transactionGuid) {
    if (buyer == null || string.IsNullOrWhiteSpace(buyer.FirstName))
        return Monad.Failure(new ValidationException("First Name Required"));

    try {
        var orderWithNewID = ... Do Heavy Lifting Here ...;
        _eventHandler.Raise("orderCreated", orderWithNewID, transactionGuid);
    }
    catch (Exception ex) {
        _eventHandler.RaiseException("orderFailure", ex, transactionGuid); // <-- should never fail BTW
        return Monad.Failure(ex);
    }
    return Monad.Success();
}

灰色の領域の問題は、それが簡単に乱用されることです。モナドに新しいOrderIDなどの返品情報を入れると、消費者は「イベントを待つことを忘れてください。私たちはここにIDがあります!!!」と言うことができます。また、すべてのコマンドがモナドを必要とするわけではありません。あなたは本当にあなたのアプリケーションの構造をチェックして、あなたが本当にエッジケースに到達したことを確認する必要があります。

モナドでは、今ではあなたのコマンド消費は次のようになるかもしれません:

//some function child in the Call Stack of "CallBackendToCreateOrder"...
    var order = CreateNewOrder(buyer, item, transactionGuid);
    if (!order.Success || order.IsExceptionState)
        ... Do Something?

遠く離れたコードベースで。 。 。

_eventHandler.on("orderCreated", transactionGuid, out order)
_storeService.PerformPurchase(order);

遠く離れたGUIで。 。 。

var transactionID = Guid.NewGuid();
OnCompletedPurchase(transactionID, x => {...});
OnException(transactionID, x => {...});
CallBackendToCreateOrder(orderDetails, transactionID);

これで、モナドのために少し灰色の領域で必要な機能と適切さをすべて備えていますが、モナドを介して誤って悪いパターンを公開していないことを確認するので、それでできることを制限します。

まあ、これはかなり古い質問ですが、私はこれをレコードのためだけに投稿します。イベントを使用するたびに、代わりにデリゲートを使用できます。多くの利害関係者がいる場合はイベントを使用します。そうでなければ、コールバックスタイルでデリゲートを使用します。

void CreateNewOrder(Customer buyer, Product item, Action<Order> onOrderCreated)

操作が失敗した場合のブロックを作成することもできます

void CreateNewOrder(Customer buyer, Product item, Action<Order> onOrderCreated, Action<string> onOrderCreationFailed)

これにより、クライアントコードの環状複雑さが減少します

CreateNewOrder(buyer: new Person(), item: new Product(), 
              onOrderCreated: order=> {...},
              onOrderCreationFailed: error => {...});

これが失われた魂を助けてくれることを願っています...

私はこれに本当に遅れていますが、言及されていないいくつかのオプションがあります(ただし、それらが本当に素晴らしいかどうかはわかりません):

これまで見たことのないオプションの1つは、コマンドハンドラーが実装する別のインターフェイスを作成することです。多分 ICommandResult<TCommand, TResult> コマンドハンドラーが実装すること。次に、通常のコマンドが実行されると、コマンド結果に結果を設定し、発信者がICommandResultインターフェイスを介して結果を引き出します。 IOCを使用すると、コマンドハンドラーと同じインスタンスを返すように、結果を引き出すことができます。しかし、これはSRPを破るかもしれません。

別のオプションは、クエリが取得できる方法でコマンドの結果をマッピングできるような共有ストアのある種の共有ストアを用意することです。たとえば、あなたのコマンドにはたくさんの情報があり、その後OperationID GUIDなどがあったとします。コマンドが終了して結果を取得すると、そのOperationID GUIDがキーとして、または別のクラスのある種の共有/静的辞書のいずれかをデータベースに押し付けます。発信者がコントロールを取り戻すと、指定されたGUIDに基づいて結果に基づいて結果を引き戻すためにクエリを呼び出します。

最も簡単な答えは、コマンド自体に結果をプッシュすることですが、それは一部の人々を混乱させるかもしれません。私が言及したもう1つのオプションは、技術的にできるイベントですが、Web環境にいる場合は、処理がはるかに困難になります。

編集

これでさらに作業した後、私は「commandquery」を作成することになりました。明らかにコマンドとクエリの間のハイブリッドです。 :)この機能が必要な場合は、使用できます。しかし、そうするための本当に正当な理由が必要です。繰り返すことはなく、キャッシュできないため、他の2つに比べて違いがあります。

コマンドクエリ分離が必要な理由について考えてください。

「システム状態の変更を心配することなく、自由にクエリを使用できます。」

そのため、コマンドから値を返して、発信者に成功したことを知らせても大丈夫です

唯一の目的のために別のクエリを作成するのは無駄だからです

前のコマンドが正しく機能したかどうかを調べます。このようなものは大丈夫です

私の本:

boolean succeeded = _storeService.PurchaseItem(buyer, item);

あなたの例の欠点は、あなたによって返されるものが明らかではないことです

方法。

string result = _storeService.PurchaseItem(buyer, item);

「結果」が正確に何であるかは明らかではありません。

CQS(コマンドクエリの分離)を使用すると、物事をより明確にすることができます

以下に似ています:

if(_storeService.PurchaseItem(buyer, item)){

    String receipt = _storeService.getLastPurchaseReciept(buyer);
}

はい、これはより多くのコードですが、何が起こっているのかより明確です。

CQSは、主にドメイン駆動型の設計を実装するときに使用されるため、(状態もodedであるように)イベント駆動型アーキテクチャを使用して結果を処理する必要があります。あなたの string result = order.Result; したがって、常にイベントハンドラーにあり、その後のコードでは直接ではありません。

チェックアウト この素晴らしい記事 これは、CQ、DDD、EDAの組み合わせを示しています。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top