集約間の参照の検証をどのように処理しますか?


11

集計間の参照で少し苦労しています。集合体Carが集合体への参照を持っていると仮定しましょうDriver。この参照は、を使用してモデル化されCar.driverIdます。

ここでの問題は、でのCar集計の作成を検証するためにどこまで行けばよいかCarFactoryです。渡さDriverIdれたものが既存のものを 参照していると信頼Driverすべきですか、それともその不変条件をチェックすべきですか?

チェックのために、私は2つの可能性を見ます:

  • 自動車工場の署名を変更して、完全なドライバーエンティティを受け入れることができます。その後、ファクトリーはそのエンティティーからIDを選択し、それを使用して車を構築します。ここでは、不変条件が暗黙的にチェックされます。
  • 私はの参照かもしれないDriverRepositoryではCarFactory、明示的に呼び出しますdriverRepository.exists(driverId)

しかし、今は不変チェックが多すぎないのではないかと思います。これらのアグリゲートが別の境界コンテキストに存在する可能性があることを想像できましたが、ドライバーBCのDriverRepositoryまたはDriverエンティティへの依存関係で車のBCを汚染します。

また、私がドメインの専門家と話をした場合、彼らはそのような参照の有効性に疑問を呈することは決してありません。ドメインモデルを無関係な問題で汚染していることを感じています。しかし、再び、ある時点でユーザー入力を検証する必要があります。

回答:


6

自動車工場の署名を変更して、完全なドライバーエンティティを受け入れることができます。その後、ファクトリーはそのエンティティーからIDを選択し、それを使用して車を構築します。ここでは、不変条件が暗黙的にチェックされます。

このアプローチは無料でチェックできるので魅力的です。また、ユビキタス言語とうまく連携しています。AはCarによって駆動されていないdriverId、しかしによりますDriver

このアプローチは、実際にそれにヴォーンバーノンで使用されているのですアイデンティティ&アクセス彼が通るサンプル有界コンテキストUserに集約しGroup、集約が、Group唯一の値型に保持していますGroupMember。ご覧のとおり、これにより彼はユーザーの有効性をチェックすることもできます(チェックが古くなっている可能性があることは十分に承知しています)。

    public void addUser(User aUser) {
        //original code omitted
        this.assertArgumentTrue(aUser.isEnabled(), "User is not enabled.");

        if (this.groupMembers().add(aUser.toGroupMember()) && !this.isInternalGroup()) {
            //original code omitted
        }
    }

ただし、Driverインスタンスを渡すことにより、Driver内部での偶発的な変更に自分自身をさらすことにもなりますCar。値の参照を渡すと、プログラマーの観点から変更を推論するのが簡単になりますが、同時にDDDはすべてユビキタス言語に関するものなので、おそらくリスクを冒す価値があります。

インターフェース分離原則(ISP)を適用するのに適切な名前を実際に思い付くことができる場合、動作メソッドを持たないインターフェースに依存する可能性があります。また、不変のドライバー参照を表し、既存のドライバー(例:)からのみインスタンス化できる値オブジェクトの概念を考え出すこともできますDriverDescriptor driver = driver.descriptor()

これらのアグリゲートが別の境界コンテキストに存在する可能性があることを想像できましたが、ドライバーBCのDriverRepositoryまたはDriverエンティティへの依存関係で車のBCを汚染します。

いいえ、実際にはそうしません。あるコンテキストの概念が別のコンテキストに流れ込まないようにするために、常に腐敗防止レイヤーがあります。あなたは車の運転手の団体専用のBCを持っている場合は、次のような既存の概念モデル化することができますので、それは実際にははるかに簡単だCarDriver特にそのコンテキストのために。

したがって、DriverLookupServiceBCで自動車と運転手の関連付けを管理する責任を定義している場合があります。このサービスは、ドライバーコンテキストによって公開されたWebサービスを呼び出す可能性があります。このサービスはDriver、このコンテキストの値オブジェクトである可能性が最も高いインスタンスを返します。

Webサービスは必ずしもBC間の最良の統合方法であるとは限らないことに注意してください。また、たとえばUserCreated、ドライバー管理コンテキストからのメッセージがリモートコンテキストで消費され、ドライバーの独自のDBにドライバーの表現を格納するメッセージングに依存することもできます。DriverLookupServiceその後、このDBを使用することができ、ドライバのデータは、さらに、メッセージ(例えばで最新の状態に保たれることになりますDriverLicenceRevoked)。

どちらのアプローチがドメインに適しているかははっきりとは言えませんが、決定を下すための十分な洞察が得られることを願っています。


3

質問する方法(および2つの代替案を提案する方法)は、自動車の作成時にdriverIdがまだ有効であることが唯一の懸念であるかのようです。

ただし、carが削除されるか別のドライバーが与えられる前に、driverIdに関連付けられたドライバーが削除されないこと(およびおそらくドライバーが別の車に割り当てられていないこと(ドメインがドライバーを1台の車に関連付けられている)。

検証の代わりに割り当てることをお勧めします(存在の検証が含まれます)。次に、割り当てられている間は削除を禁止します。これにより、構築中の古いデータの競合状態や他の長期的な問題から保護されます。(割り当ては、割り当てを検証およびマークし、アトミックに動作することに注意してください。)

ところで、私は@PriceJonesに同意します。車とドライバーの関連付けは、おそらく車またはドライバーのどちらかとは別の責任であるということです。この種の関連付けは、スケジュールの問題(ドライバー、車、タイムスロット/ウィンドウ、代替など)のように聞こえるため、時間の経過とともに複雑さが増すだけです。登録の問題に似ていても、登録および現在の登録。したがって、それはそれ自身のBCを完全に十分に価値があるかもしれません。

割り当てられている集計エンティティのBC内、または別のBC、たとえば自動車と運転者の関連付けを行うものなど、割り当てスキーム(ブール値や参照カウントなど)を提供できます。前者を実行すると、自動車または運転手BCに対して発行された(有効な)削除操作を許可できます。後者を行う場合は、車とドライバーのBCからの削除を防止し、代わりに車とドライバーの関連付けスケジューラを介してそれらを送信する必要があります。

次のように、BC間で割り当ての責任の一部を分割することもできます。車とドライバーのBCはそれぞれ、割り当てられたブール値をそのBCで検証および設定する「割り当て」スキームを提供します。それらの割り当てブールが設定されると、BCは対応するエンティティの削除を防ぎます。(そして、システムは、車とドライバーのBCが車とドライバーの関連付けのスケジューリングBCからの割り当てと割り当て解除のみを許可するように設定されています。)

BCをスケジュールする車とドライバーは、現在および将来のある期間/期間、車に関連付けられたドライバーのカレンダーを維持し、スケジュールされた車またはドライバーの最後の使用についてのみ、他のBCに割り当て解除を通知します。


より根本的な解決策として、車とドライバーのBCを追加専用の履歴レコード工場として扱い、車とドライバーの関連付けスケジューラーに所有権を任せることができます。車BCは、VINとともに車のすべての詳細を備えた新しい車を生成する場合があります。車の所有権は、車/ドライバー協会のスケジューラーによって処理されます。車/ドライバーの関連付けが削除され、車自体が破棄された場合でも、車のレコードは定義により車のBCに存在し、車のBCを使用して履歴データを検索できます。自動車/ドライバー協会/所有権(過去、現在、そして将来的には予定されている)が別のBCによって処理されている間。


2

集約Carが集約Driverへの参照を持っていると仮定しましょう。この参照は、Car.driverIdを使用してモデル化されます。

うん、それは1つの集合体を別の集合体に結合する正しい方法です。

私がドメインの専門家と話をするなら、彼らはそのような参照の妥当性を決して質問しないでしょう

ドメインの専門家に質問するのは適切ではありません。「ドライバーが存在しない場合のビジネスのコストはいくらですか?」

おそらくDriverRepositoryを使用してdriverIdをチェックしません。代わりに、ドメインサービスを使用してそれを行います。私はそれが意図を表現するより良い仕事をすると思います-カバーの下で、ドメインサービスは記録システムをまだチェックします。

だから何か

class DriverService {
    private final DriverRepository driverRepository;

    boolean doesDriverExist(DriverId driverId) {
        return driverRepository.exists(driverId);
    }
}

実際に、さまざまなポイントでdriverIdについてドメインをクエリします

  • クライアントから、コマンドを送信する前に
  • アプリケーションで、コマンドをモデルに渡す前
  • ドメインモデル内、コマンド処理中

これらのチェックの一部またはすべては、ユーザー入力のエラーを減らすことができます。しかし、それらはすべて古いデータから機能しています。他の集計は、質問した直後に変わる可能性があります。そのため、偽陰性/陽性の危険が常にあります。

  • 例外レポートでは、コマンドが完了した後に実行します

ここでは、まだ古いデータを使用しています(レポートの実行中に集計がコマンドを実行している場合があり、すべての集計への最新の書き込みを確認できない場合があります)。しかし、アグリゲート間のチェックが完全になることは決してありません(Car.create(driver:7)がDriver.delete(driver:7)と同時に実行される)。これにより、リスクに対する追加の防御層が得られます。


1
Driver.delete存在すべきではありません。アグリゲートが破壊されるドメインを見たことはありません。ARを維持することで、孤児になることはありません。
plaxは2016年

1

それは質問するのを助けるかもしれません:あなたは自動車が運転手で造られることを本当に確信していますか?実世界では、ドライバーで構成された車を聞いたことがありません。この質問が重要である理由は、車とドライバーを個別に作成してから、ドライバーを車に割り当てる外部メカニズムを作成する方向を示す可能性があるためです。車はドライバーの参照なしで存在でき、有効な車であり続けることができます。

車があなたの文脈で絶対にドライバーを持っている必要があるならば、あなたはビルダーパターンを考慮することを望むかもしれません。このパターンは、自動車が既存のドライバーで構築されることを保証する責任があります。工場は個別に検証された車とドライバーを提供しますが、ビルダーは、車を提供する前に、必要な参照が車にあることを確認します。


車とドライバーの関係についても考えましたが、DriverAssignment集計を導入すると、検証する必要がある参照が移動するだけです。
VoiceOfUnreason 2016年

1

しかし、今は不変チェックが多すぎないのではないかと思います。

私はそう思う。DBから特定のDriverIdをフェッチすると、存在しない場合は空のセットが返されます。したがって、返された結果を確認することで、それが存在するかどうか(そしてフェッチするかどうか)を尋ねる必要がなくなります。

その後、クラス設計はそれも不要にします

  • 「駐車中の車にはドライバーがいる場合といない場合がある」という要件がある場合
  • Driverオブジェクトがを必要DriverIdとし、コンストラクターで設定されている場合。
  • Carだけが必要な場合DriverIdは、Driver.Idゲッターを用意します。セッターなし。

リポジトリはビジネスルールの場所ではありません

  • Car心配事、それは持っている場合にはDriver(少なくとも、または自分のIDを)。AのDriver心配事、それが持っている場合DriverIdRepositoryデータの整合性を気にし、ドライバレス車についてはあまり気にしませんでした。
  • DBにはデータ整合性ルールがあります。null以外のキー、null以外の制約など。ただし、データの整合性はデータ/テーブルスキーマに関するものであり、ビジネスルールに関するものではありません。この場合、強い相関の共生関係がありますが、この2つを混同しないでください。
  • a DriverIdがビジネスドメインのものであることは、適切なクラスで処理されます。

懸念の分離違反

... Repository.DriverIdExists()質問をすると発生します。

ドメインオブジェクトを作成します。そうでない場合は、Driver多分DriverInfo(ちょうどa DriverIdName、言っておきましょう)オブジェクトです。DriverId建設時に検証されます。それは存在し、正しいタイプである必要があります。次に、存在しないdriver / driverIdをどのように処理するかは、クライアントクラス設計の問題です。

たぶん、Carあなたが電話するまで、ドライバーがいなくても大丈夫ですCar.Drive()。その場合、Carオブジェクトはもちろんそれ自身の状態を保証します。なしでは運転できませんDriver-まあ、まだです。

プロパティをクラスから分離することは悪いことです

もちろん、ごCar.DriverId希望があればお持ちください。しかし、次のようになります。

public class Car {
    // Non-null driver has a driverId by definition/contract.
    protected DriverInfo myDriver;
    public DriverId {get { return myDriver.Id; }}

    public void Drive() {
       if (myDriver == null ) return errorMessage; // or something
       // ... continue driving
    }
}

これではない:

public class Car {
    public int DriverId {get; protected set;}
}

今度はCar、すべてのDriverId有効性の問題に対処する必要があります-単一の責任原則違反。冗長コードでしょう。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.