「その他」の集合体の状態をどこで検証する必要がありますか?


8

シナリオ:

顧客が注文し、製品を受け取った後、注文プロセスに関するフィードバックを提供します。

次の集計ルートを想定します。

  • お客様
  • 注文
  • フィードバック

ビジネスルールは次のとおりです。

  1. 顧客は自分の注文についてのみフィードバックを提供でき、他の人のフィードバックは提供できません。
  2. 顧客は、注文に対する支払いが済んでいる場合にのみフィードバックを提供できます。

    class Feedback {
        public function __construct($feedbackId,
                                    Customer $customer,
                                    Order $order,
                                    $content) {
            if ($customer->customerId() != $order->customerId()) {
                // Error
            }
            if (!$order->isPaid()) {
                // Error
            }
            $this->feedbackId = $feedbackId;
            $this->customerId = $customerId;
            $this->orderId = $orderId;
            $this->content = $content;
        }
    }
    

ここで、企業が新しいルールを望んでいると想定します。

  1. 顧客Supplierは、注文の商品がまだ稼働している場合にのみフィードバックを提供できます。

    class Feedback {
        public function __construct($feedbackId,
                                    Customer $customer,
                                    Order $order,
                                    Supplier $supplier,
                                    $content) {
            if ($customer->customerId() != $order->customerId()) {
                // Error
            }
            if (!$order->isPaid()) {
                // Error
            }
            // NEW RULE HERE
            if (!$supplier->isOperating()) {
                // Error
            }
            $this->feedbackId = $feedbackId;
            $this->customerId = $customerId;
            $this->orderId = $orderId;
            $this->content = $content;
        }
    }
    

最初の2つのルールの実装をFeedback 集約自体の中に配置しました。特に、Feedback集合体が他のすべての集合体をIDで参照しているので、これを行うのは快適 です。たとえば、Feedbackコンポーネントのプロパティは、他の集約の存在を知っていることを示している ので、これらの集約の読み取り専用の状態も知っておくと安心です。

ただし、そのプロパティに基づいて、Feedbackアグリゲートはアグリゲートの存在を認識していないSupplierため、このアグリゲートの読み取り専用の状態認識して いる必要がありますか?

ルール3を実装する別のソリューションは、このロジックを適切なに移動することCommandHandlerです。しかし、これはドメインロジックを私のオニオンベースのアーキテクチャの「中心」から遠ざけているように感じます。

私のタマネギのアーキテクチャの概要


リポジトリインターフェースはドメインの一部です。したがって、構築ロジック(それ自体がDDDブックのサービスと見なされます)は、注文のリポジトリーを呼び出して、注文のサプライヤーがまだ稼働しているかどうかを尋ねることができます。
16:05の陶酔的な

まず、Supplierアグリゲートの動作状態は、Orderリポジトリを介して照会されません。SupplierおよびOrder2つの別個の集合体です。次に、DDD / CQRSメーリングリストで、集約ルートとリポジトリを他の集約ルートメソッド(コンストラクタを含む)に渡すことについて質問がありました。さまざまな意見がありましたが、グレッグヤングは、集約ルートをパラメーターとして渡すことは一般的であると述べましたが、別の人は、リポジトリーはドメインよりもインフラストラクチャーに密接に関連していると述べました。たとえば、リポジトリは「メモリコレクションで抽象化」され、ロジックがありません。
magnus 2016年

サプライヤーは注文に関連していませんか?注文に関係のないサプライヤーが渡された場合はどうなりますか?まあ、「サプライヤーは運営しています」は論理ではありません。簡単なクエリです。また、これが一般的である理由もあります。これがないと、コードがはるかに複雑になり、エラーが発生する可能性のある情報を渡す必要があります。また、「リポジトリインターフェース」はインフラストラクチャではありません。リポジトリの実装です。
陶酔的な

あなたが正しい。Customerが自分の注文の1つにのみフィードバックを提供できるように($order->customerId() == $customer->customerId())、サプライヤーID($order->supplierId() == $supplier->supplierId())も比較する必要があります。最初のルールは、ユーザーが誤った値を入力するのを防ぎます。2番目のルールは、プログラマーが誤った値を指定しないようにします。それにもかかわらず、サプライヤが動作しているかどうかのチェックは、Feedbackエンティティまたはコマンドハンドラのいずれかで行う必要があります。質問はどこですか。
magnus 2016年

2
質問に直接関連しない2つのコメント。まず、集約ルートを別の集約への引数として渡すと、見た目が正しくありません。それらはIDである必要があります。集約が別の集約に対して実行できる便利なものはありません。第2に、顧客とサプライヤーは...難しいです。どちらの場合も記録簿は現実の世界です。CeaseOperationsコマンドをドメインモデルに送信して、現実の世界のサプライヤーを停止することはできません。
VoiceOfUnreason 2016年

回答:


1

トランザクションの正確さのために、別のアグリゲートの現在の状態を知っているアグリゲートが必要な場合は、モデルが間違っています。

ほとんどの場合、トランザクションの正確性は必要ありません。企業は、レイテンシと古いデータを許容する傾向があります。これは、検出と修正が容易な不整合に特に当てはまります。

したがって、コマンドは状態を変更するアグリゲートによって実行されます。必ずしも正しいとは限らないチェックを実行するには、必ずしも他のアグリゲートの状態の最新のコピーが必要ではありません。

既存の集約のコマンドの場合、通常のパターンはリポジトリを集約に渡し、集約はその状態をリポジトリに渡します。これにより、他の集約の不変の状態/予測を返すクエリが提供されます

class Feedback {
    void downvote(Repository<Supplier.State> query) {
        Supplier.State supplier = query.getById(this->supplierId);
        boolean isOperating = state.isOperating();
        ....
    }
}

しかし、構築パターンは奇妙です。オブジェクトを作成しているとき、呼び出し側は内部状態を提供しているので、内部状態をすでに知っています。同じパターンが機能し、それは無意味に見えます

class Feedback {
    __construct(SupplierId supplierId, SupplierOperatingQuery query ...) {
        Supplier.State supplier = query.getById(this->supplierId);
        boolean isOperating = state.isOperating();
        ....
    }
}

すべてのドメインロジックをドメインオブジェクトに保持することでルールを守っていますが、そうすることでビジネスの不変条件を実際に保護することはしません(アプリケーションコンポーネントが同じ情報をすべて利用できるため)。作成パターンについては、次のように記述してもよいでしょう

class Feedback {
    __construct(Supplier.State supplier, ...) {
        boolean isOperating = state.isOperating();
        ....
    }
}

1. SupplierOperatingQueryクエリが読み取りモデル、または名前の「クエリ」を誤解させるものですか?2.トランザクションの整合性は必要ありません。顧客がフィードバックを残す1秒前にサプライヤが操作を停止しても問題ありませんが、それはとにかくそれをチェックするべきではないということですか?3.あなたの例では、オブジェクト自体ではなく「クエリサービス」を提供することで、トランザクションの一貫性が強化されますか?もしそうなら、どうですか?4.このようなクエリサービスを使用すると、単体テストにどのような影響がありますか?
magnus 2016年

1.クエリ。呼び出しても何も変化しないという意味で。3.クエリサービスとのトランザクションの一貫性はありません。クエリサービスと、他の集計を変更している同時に実行されているコマンドとの相互作用はありません。4.この場合、ドメインモデルのspiの一部になるため、テスト実装を提供します。ええと、それは少し奇妙なことです-DomainServiceは、使用するのに最適な用語ではないかもしれません。
VoiceOfUnreason 2016年

2.ここで使用しているデータは集計の境界を越えているため、小切手が間違った答えを出す可能性があることに注意してください(たとえば、小切手は問題があると言っていますが、他の集計が変更されているためです)。そのため、そのチェックを読み取りモデルに移動することをお勧めします(常にコマンドを受け入れますが、モデルに一貫性がない場合は例外レポートを作成します)。クライアントが成功するはずのコマンドのみを送信するように設定することもできます。つまり、クライアントは、現在の状態の理解に基づいて、失敗すると予想されるコマンドを送信してはいけません。
VoiceOfUnreason 2016年

1.通常、「書き込み側」が「読み取り側」にクエリを実行することは避けられます(たとえば、イベントソースの予測)。「...呼び出しても何も変わらないという意味で」-不変のアクセサーを使用するだけではなく、はるかに単純であると私は主張します。2. 読み取りモデルでチェックを複製することは問題ありませんが、それを移動すると(読み取り:サーバーから削除)、問題が発生します。まず、ビジネスルールを各クライアント(Webブラウザーとモバイルクライアント)で複製する必要があります。次に、このチェックをバイパスするのは簡単です:
magnus

3.「...他の集約を変更する同時実行コマンドとの間に相互作用はありません」-フィードバック集約のみが変更されているため、サプライヤ集約自体のロードも行いません。4.したがって、SupplierOperatingQueryは具体的な実装を必要とするインターフェイスです。つまり、ユニットテストでモック実装を作成して、他のオブジェクトに既に存在する単一の変数のtrue / false値をテストする必要がありますか?やり過ぎのようなにおい。CustomerOwnsOrderQueryとOrderIsPaidQueryも作成しないのはなぜですか?
magnus 2016年

-1

これは古い質問であることは承知していますが、この問題は誤った前提に直接起因していることを指摘しておきます。つまり、存在することを想定している集計ルートは、単に正しくありません。

説明したシステムに存在する集約ルートは、Customerのみです。注文とフィードバックの両方は、それ自体が集合体である場合がありますが、存在を顧客に依存しているため、それ自体が集合体の根ではありません。フィードバックコンストラクターで提供するロジックは、注文にcustomerIdがあり、フィードバックは顧客に関連付けられている必要があることを示しているようです。意味あり。注文やフィードバックを顧客に関連させないようにするにはどうすればよいですか?さらに、サプライヤーは論理的に注文に関連しているようです(したがって、この集計内にあります)。

上記を念頭に置いて、必要な情報はすべてCustomer集約ルートですでに利用可能であり、ルールを間違った場所に適用していることが明らかになります。コンストラクターはビジネスルールを適用するためのひどい場所であり、すべてのコストで回避する必要があります。これは次のようになります(注:ファクトリーを使用する必要があるため、CustomerおよびOrde​​rのコンストラクターは含めません。すべてのインターフェースメソッドも表示していません)。

/*******************************\
   Interfaces, explained below 
\*******************************/

interface ICustomer
{
    public function getId() : int;
}

interface IUser extends ICustomer
{
    public function getUsername() : string;

    public function getPassword() : string;

    public function changeUsername( string $new ) : void;

    public function resetPassword( string $new ) : void;

}

interface IReviewer extends ICustomer
{
    public function provideFeedback( IOrder $order, string $content ) : void;
}

interface IBuyer extends ICustomer
{
    public function placeOrder( IOrder $order ) : void;
}

interface IOrder
{
    public function getCustomerId() : int;

    public function addFeedback( string $content ) : void;
}


interface IFeedback
{
    public function addContent( string $content ) : void;

    public function isValidContent( string $content ) : void;
}



/*******************************\
   Implentation
\*******************************/



class Customer implements IReviewer, IBuyer
{
    protected $id;

    protected $orders = [];

    public function provideFeedback( IOrder $order, string $content ) : void
    {
        if( $order->getCustomerId() !== $this->getId() )
            throw new \InvalidArgumentException('Customers can only provide feedback on their own orders');

        $order->addFeedback( $content );
    }
}


class Order implements IOrder
{
    protected $supplier;

    protected $feedbacks = [];

    public function addFeedback( string $content ) : void
    {
        if( false === $this->supplier->isOperating() )
            throw new \Exception('Feedback can only be added to orders if the supplier is still operating.');

        // could be any IFeedback
        $feedback = new Feedback( $this );

        $feedback->addContent( $content );

        $this->feedbacks[] = $feedback;
    }
}


class Feedback implements IFeedback
{
    protected $order;

    protected $content;

    public function __construct( IOrder $order )
    {    
         // we don't carry our business rules in constructors
         $this->order = $order;
    }

    public function addContent( string $content ) : void
    {
        if( false === $this->isValidContent($content) )
            throw new \Exception("Content contains offensive language.");

        $this->content = $content;
    }
}

はい。これを少し分解してみましょう。最初に気付くのは、このモデルがどれほど宣言的であるかです。すべてがアクションであり、ビジネスルールが適用される場所が明らかになります。上記のデザインは、正しいことを「行う」だけでなく、正しいことを「言う」ものです。

ルールが次の行で実行されていると誰が思い込むのでしょうか?

// this is a BAD place for rules to execute
$feedback = new Feedback( $id, $customerId, $order, $supplier, $content);

2番目に、ビジネスルールの検証に関連するすべてのロジックが、それらが関連するモデルに可能な限り厳密に実行されていることがわかります。あなたの例では、コンストラクター(単一のメソッド)が異なるモデルに対して複数の検証を実行しています。それはSOLID設計を壊します。フィードバックコンテンツに不適切な単語が含まれていないことを確認するためのチェックをどこに追加しますか?コンストラクタの別のチェック?異なる種類のフィードバックに異なるコンテンツチェックが必要な場合はどうなりますか?醜い。

3番目に、インターフェースを見ると、構成を通じてルールを拡張/変更するための自然な場所があることがわかります。たとえば、フィードバックの提供時期については、注文の種類ごとに異なるルールを設定できます。Orderは、さまざまな種類のフィードバックを提供することもできます。そのフィードバックは、検証のためのさまざまなルールを持つことができます。

また、多数のICustomer *インターフェイスも確認できます。これらは、ここで必要なCustomer集計を作成するために使用されます(おそらく単にCustomerと呼ばれるだけではありません)。この理由は簡単です。お客様がドメイン/ DB全体に広がる巨大な集約ルートである可能性が非常に高いです。インターフェースを使用することにより、1つの集合体(ロードするには大きすぎる可能性がある)を、特定のアクション(順序付けやフィードバックの提供など)のみを提供する複数の集合体ルートに分解できます。私の実装の集計は、注文とフィードバックの両方を行うことができますが、パスワードのリセットやユーザー名の変更には使用できません。

したがって、あなたの質問への答えは、集計は自分自身を検証する必要があるということです。できない場合は、モデルが不足している可能性があります。


1
集計の境界はシステムを設計している人によって異なりますが、秩序から生じる「1つの集計」はばかげていると思います。サプライヤが注文の一部であるというあなたの例は、良い例です-注文が作成されるまでサプライヤは存在できませんか?重複するサプライヤーについては:
magnus

@ user1420752私はあなたがそれを逆に持っているかもしれないと思います。上記のモデルは逆を意味します。注文はサプライヤーなしでは存在できません。私の例では、提供されたコードから収集できる情報/ルール/関係を単に使用しています。Ordererは、顧客と同様に、それ自体が(ルートではなく)大きく複雑な集合体である可能性が高いことに同意します。コンテキストに応じて、少数の具体的な実装への分解が必要になる場合もあります。私が説明しているポイントは、エンティティは自身を検証する必要があるということです。ご覧のとおり、その方がきれいです。
キングサイドスライド

@ user1420752多くの引数を必要とするメソッド/コンストラクターは、データが動作から分離されている貧血モデルの兆候であることを追加したいと思います(したがって、データに作用する部分に大きなチャックに注入する必要があります)。あなたが提供したフィードバックコンストラクタはこの例です。貧血モデルは、まとまりを減らし、余分な結合セマンティクスを追加する傾向があります(IDを何度もチェックするなど)。凝集度が高いということは、通常、エンティティ内のすべてのメソッドがそのインスタンス変数をすべて利用することを意味します。これは当然、CustomerやOrderのような大きな集合体の分解につながります
キングサイドスライド
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.