web-dev-qa-db-ja.com

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

ウィキペディアの コマンドクエリの分離 の定義では、

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

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

例えば:

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

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

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

しかし、これはコードのサイズと複雑さを増しているように思われますが、これはリファクタリングの方向としては非常に前向きではありません。

操作の結果が必要なときに、誰かが私にコマンドとクエリの分離を実現するより良い方法を教えてもらえますか?

ここで何か不足していますか?

ありがとう!

注:Martin Fowlerはこれをcqsの制限について述べています CommandQuerySeparation

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

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

64
Mark Rogers

この質問は古くなっていますが、満足のいく回答はまだ得られていないため、ほぼ1年前のコメントについて少し詳しく説明します。

イベント駆動型アーキテクチャの使用は、コマンドとクエリを明確に分離するだけでなく、新しいアーキテクチャの選択肢が開かれ、通常は非同期プログラミングモデルに適合するため(アーキテクチャをスケーリングする必要がある場合に便利)、非常に理にかなっています。たいていの場合、解決策はドメインのモデリングを変えることにあるかもしれません。

それでは、購入の例を見てみましょう。 StoreService.ProcessPurchaseは、購入処理に適したコマンドです。これはPurchaseReceiptを生成します。これは、領収書をOrder.Resultで返すよりも良い方法です。物事を非常に単純に保つために、コマンドからレシートを返し、ここでCQRSに違反することができます。より明確な分離が必要な場合、このコマンドは、サブスクライブできるReceiptGeneratedイベントを発生させます。

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

41
17
Masoud

上記のCQSとCQRSの間で多くの混乱が見られます(Mark Rogersも1つの回答に気づきました)。

CQRSはDDDのアーキテクチャアプローチであり、クエリの場合、すべてのエンティティと値タイプを持つ集約ルートから完全なオブジェクトグラフを構築するのではなく、リストに表示する軽量のビューオブジェクトのみを構築します。

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

質問に答えるには、コマンドの実行と結果の取得を回避する方法を考えることができます。たとえば、void実行メソッドと読み取り専用のコマンド結果プロパティを持つコマンドオブジェクト(コマンドパターン)を提供します。

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

ちなみに、このルールに厳密に従うことは、フルーエントAPIの原則全体を破るものではないでしょうか?

13
Remco te Wierik

さて、これはかなり古い質問ですが、私はこれを記録のために投稿します。イベントを使用するときはいつでも、代わりにデリゲートを使用できます。多くの関係者がいる場合はイベントを使用し、そうでない場合はコールバックスタイルでデリゲートを使用します。

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 => {...});

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

2
max_cervantes

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

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

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

モナドが可能です。コマンドが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);

これで、モナドの灰色の領域が少しあるだけで、必要なすべての機能性と適切性が得られましたが、モナドを通して誤ったパターンを誤って公開していないことを確認しているため、モナドでできることを制限しています。

2
Suamere

私は他の人々が与えたイベント駆動型アーキテクチャーの提案が好きですが、私は別の視点を取り入れたいと思っています。コマンドから実際にデータを返す理由を調べる必要があるかもしれません。実際に結果が必要ですか、それとも失敗した場合に例外をスローしてもかまいませんか?

これを普遍的な解決策と言っているわけではありませんが、「応答を送り返す」モデルではなく、より強力な「失敗時の例外」モデルに切り替えると、実際の分離が実際のコードで機能するようになりました。もちろん、結局はさらに多くの例外ハンドラーを作成しなければならないので、それはトレードオフです...しかし、それは考慮すべき少なくとも別の角度です。

2
Bryce Wagner

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

これまで見たことがないオプションの1つは、コマンドハンドラーが実装する別のインターフェイスを作成することです。多分 ICommandResult<TCommand, TResult>コマンドハンドラが実装します。次に、通常のコマンドが実行されると、コマンドの結果に結果が設定され、呼び出し元はICommandResultインターフェイスを介して結果を引き出します。 IoCを使用すると、コマンドハンドラーと同じインスタンスを返すようにして、結果を引き出すことができます。ただし、これによりSRPが壊れる可能性があります。

別のオプションは、クエリが取得できる方法でコマンドの結果をマップできる、ある種の共有ストアを持つことです。たとえば、コマンドに大量の情報があり、次にOperationId Guidまたはそのようなものがあるとします。コマンドが終了して結果を取得すると、そのOperationId GUIDをキーとしてデータベースに回答をプッシュするか、別のクラスのある種の共有/静的ディクショナリに回答をプッシュします。呼び出し元が制御を取り戻すと、クエリを呼び出して、指定されたGuidに基づく結果に基づいてプルバックします。

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

編集

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

1
Daniel Lorenz

コマンドクエリの分離が必要な理由について、もう少し時間をかけて考えてください。

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

したがって、コマンドから値を返し、呼び出し元に成功したことを知らせるのは問題ありません。

の目的のためだけに別のクエリを作成するのは無駄です。

前のコマンドが適切に機能したかどうかを確認します。このようなもので大丈夫です

私の本:

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

あなたの例の欠点は、あなたが返すものが明らかでないことです

方法。

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

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

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

以下と同様:

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

    String receipt = _storeService.getLastPurchaseReciept(buyer);
}

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

1
kiwicomb123

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

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

0
Jan Jongboom