69

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

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

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

例えば:

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

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

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

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

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

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

ありがとう!

注:Martin Fowlerは、 cqsCommandQuerySeparationの制限について次のように述べています。

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

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

4

9 に答える 9

45

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

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

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

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

于 2011-07-15T08:03:19.587 に答える
20

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

CQRSは、DDDのアーキテクチャアプローチであり、クエリの場合、すべてのエンティティと値型を含む集計ルートから本格的なオブジェクトグラフを作成するのではなく、リストに表示する軽量のビューオブジェクトのみを作成します。

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

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

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

ちなみに、このルールに厳密に従うことは、流暢なAPIの原則全体を破ることはありませんか?

于 2015-09-09T08:43:20.820 に答える
3

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

答えは:あなたはしません。コマンドを実行して結果を取得する場合は、CQSを使用していません。

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

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

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

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);

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

于 2017-05-16T19:19:43.593 に答える
3

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

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

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

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

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

私の本:

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

あなたの例の不利な点は、あなたが何を返すのかが明確でないことです

方法。

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

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

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

以下のように:

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

    String receipt = _storeService.getLastPurchaseReciept(buyer);
}

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

于 2018-08-14T22:14:47.510 に答える
3

ああ、それは面白い。たぶん私にも言いたいことがあります。

最近、私は非正統的なCQS(誰かにとってはCQSではないかもしれませんが、私はあまり気にしません)アプローチを使用しており、(仕様パターンを使用しているので)厄介なリポジトリの実装とサービスを回避するのに役立ちます特に巨大なプロジェクトでは、時間の経過とともに非常に大きく成長するレイヤークラス。問題は、他のすべてが正常で、開発者がかなり熟練している場合でも発生することです。なぜなら、(驚いたことに)大きなクラスを持っている場合、そもそもSRPに違反しているとは限らないからです。そして、そのようなプロジェクトでよく見られるアプローチは、「ああ、私たちは巨大なクラスを持っているので、それらを分割しましょう」であり、その分割は自然に進化するのではなく、ほとんどが合成です。それで、人々はこれに対処するために何をしますか?彼らは1つからいくつかのクラスを作ります。しかし、突然、以前の数倍のクラスが発生した場合、巨大なプロジェクトのDIはどうなりますか?DIにはおそらくすでにインジェクションがかなりロードされているので、あまり良い画像ではありません。したがって、ファサードパターンなど(該当する場合)などの回避策があり、その意味するところは次のとおりです。問題を防止しないでください。結果のみに対処し、それに多くの時間を費やします。多くの場合、リファクタリングには「合成」アプローチを使用します。より多くの悪ではなくより少ない悪を得るが、それでもそれは悪である。

代わりに何をしますか?最初のステップとして、KISSとYAGNIをCQSに適用します。

  1. コマンド/コマンドハンドラーとクエリ/クエリハンドラーを使用します。
  2. 結果とエラーを含むクエリとコマンドの両方に汎用リターンオブジェクトを使用します(痛い!)。
  3. 厳密に必要な場合にのみ、デフォルトで標準のサービスとリポジトリの実装を避けてください。

このアプローチでどのような問題が解決されますか?

  1. コードの混乱を早期に防止し、操作と拡張がはるかに簡単になります(将来にわたって利用できます)。
  2. 信じられないかもしれませんが、中規模のプロジェクトでは、サービスクラスもリポジトリもまったくありませんでした。プロジェクトが大きいほど、そのようなアプローチはより有益です(CQRSとESが不要であり、標準のサービス+データレイヤーのみと比較する場合)。また、コストと効率の点で、ほとんどの中規模プロジェクトには十分すぎるため、非常に満足しています。

それで、私はあなたに何をすることを提案しますか?

  1. 適切な仕事に適切なツールを使用してください。あなたの問題を解決するアプローチを使用し、「それが理由であるという理由だけで」あなたのケースに不必要な複雑さが伴う場合は、本ですべてを行うことを避けてください。ちなみに、完全にRESTfulなレベル3 APIを目にする頻度はどれくらいですか?..
  2. 必要がない場合、特に理解していない場合は何も使用しないでください。本当に必要がない場合は、利益よりも害が大きくなります。CQRSは場合によっては適切であり、それでもかなり理解しやすいですが、開発とサポートには代償が伴います。ESは理解するのがかなり難しく、構築とサポートがさらに困難です。
于 2020-10-10T15:17:25.280 に答える
2

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

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

于 2010-09-21T23:08:22.957 に答える
2

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

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

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

于 2018-08-14T22:00:59.103 に答える
1

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

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

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

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

編集

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

于 2017-07-18T18:52:14.020 に答える
0

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

CQS、DDD、EDAの組み合わせを示すこの素晴らしい記事をチェックしてください。

于 2010-09-13T15:25:26.073 に答える