ビジネスロジックをサービスレイヤーに移動せずに、ドメインオブジェクト属性の一意の制約を確認するための洗練された方法はありますか?


10

私はドメイン駆動設計を約8年間適応してきましたが、これらすべての年が経過した後でも、まだ1つ問題があります。それは、ドメインオブジェクトに対してデータストレージ内の一意のレコードをチェックしています。

2013年9月、Martin Fowlerは、可能であればすべてのドメインオブジェクトに適用する必要があるTellDon'tAsk原則について言及し、操作がどのように行われたかをメッセージで返します(オブジェクト指向の設計では、これはほとんど例外によって行われます)。操作は失敗しました)。

私のプロジェクトは通常多くの部分に分かれており、そのうちの2つはドメイン(ビジネスルールのみを含み、ドメインは完全に永続性を無視する)とサービスです。データをCRUDするために使用されるリポジトリレイヤーを認識するサービス。

オブジェクトに属している属性の一意性はドメイン/ビジネスルールであるため、ドメインモジュールにとっては長くなければならないため、ルールは本来あるべき場所にあります。

レコードの一意性を確認できるようにするには、現在のデータセット(通常はデータベース)にクエリを実行して、別のレコードNameが存在するかどうかを確認する必要があります。

ドメインレイヤーは永続性を無視しており、データを取得する方法がわからないだけでなく、データを操作する方法しか考慮していないため、リポジトリ自体に直接触れることはできません。

私が適応させたデザインは次のようになります。

class ProductRepository
{
    // throws Repository.RecordNotFoundException
    public Product GetBySKU(string sku);
}

class ProductCrudService
{
    private ProductRepository pr;

    public ProductCrudService(ProductRepository repository)
    {
        pr = repository;
    }

    public void SaveProduct(Domain.Product product)
    {
        try {
            pr.GetBySKU(product.SKU);

            throw Service.ProductWithSKUAlreadyExistsException("msg");
        } catch (Repository.RecordNotFoundException e) {
            // suppress/log exception
        }

        pr.MarkFresh(product);
        pr.ProcessChanges();
    }
}

これにより、ドメインレイヤー自体ではなくサービスがドメインルールを定義し、コードの複数のセクションにルールが分散されます。

明確にわかるように、サービスはアクションを提供するので(それはを保存するProductか、例外をスローします)、TellDon'tAskの原則について述べましたが、メソッド内では、手続き型のアプローチを使用してオブジェクトを操作しています。

明白な解決策は、をスローDomain.ProductCollectionするAdd(Domain.Product)メソッドを使用してクラスを作成するProductWithSKUAlreadyExistsExceptionことですが、コード内で製品がすでに同じSKUを持っているかどうかを確認するには、データストレージからすべての製品を取得する必要があるため、パフォーマンスが大幅に不足しています追加しようとしている製品として。

この特定の問題をどのように解決しますか?これ自体は問題ではありません。何年もの間、特定のドメインルールを表すサービスレイヤーを用意してきました。サービス層は通常、より複雑なドメイン操作にも対応します。キャリアの中で、より優れた、より集中化されたソリューションに出くわしたのではないかと思います。


名前を要求して、見つかったかどうかに基づいてロジックを作成できるのに、なぜ名前のリスト全体をプルするのですか?インターフェースとの合意がある限り、永続層に何をすべきかではなく、何をすべきかを指示します。
JeffO 2016年

1
サービスという用語を使用するときは、ドメインレイヤーのドメインサービスとアプリケーションレイヤーのアプリケーションサービスの違いなど、より明確に向けて努力する必要があります。gorodinski.com/blog/2012/04/14/…を
Erik Eidt '17年

優れた記事、@ ErikEidt、ありがとう。私のデザインは間違っていないと思います、ゴロディンスキー氏を信頼できるなら、彼はほとんど同じことを言っています:より良い解決策は、アプリケーションサービスがエンティティに必要な情報を取得し、実行環境を効果的にセットアップして、それをエンティティに送信し、リポジトリをドメインモデルに直接注入することに対して、私と同じように反対しています。
アンディ

「教えてください」の引用からの引用: 「個人的には、tell-dont-askは使用しません。」
レーダーボブ2016年

回答:


7

ドメインレイヤーは永続性を無視しており、データを取得する方法がわからないだけでなく、データを操作する方法しか考慮していないため、リポジトリ自体に直接触れることはできません。

私はこの部分に同意しません。特に最後の文。

ドメインは永続性を無視する必要があることは事実ですが、「ドメインエンティティのコレクション」があることは知っています。そして、このコレクション全体に関係するドメインルールがあります。ユニークさはそれらの1つです。実際のロジックの実装は特定の永続化モードに大きく依存するため、このロジックの必要性を指定するある種の抽象化がドメインに存在する必要があります。

そのため、名前が既に存在するかどうかを照会できるインターフェースを作成するのと同じくらい簡単です。これは、データストアに実装され、名前が一意かどうかを知る必要がある人が呼び出すことができます。

そして、リポジトリはドメインサービスであることを強調したいと思います。それらは永続化に関する抽象化です。ドメインとは別のリポジトリの実装です。ドメインエンティティがドメインサービスを呼び出すことには何の問題もありません。リポジトリを使用して別のエンティティを取得したり、メモリに簡単に保持できない特定の情報を取得したりできることに問題はありません。これが、Evansの本でリポジトリが重要な概念である理由です。


ご入力ありがとうございます。Domain.ProductCollectionドメインレイヤーからオブジェクトを取得する責任があることを考えると、リポジトリは実際に私が考えていたものを表すことができますか?
アンディ

@DavidPacker私はあなたが何を意味するのか本当に理解できません。なぜなら、すべてのアイテムをメモリに保持する必要はないはずだからです。"DoesNameExist"メソッドの実装は、(おそらく)データストア側のSQLクエリである必要があります。
16:35の陶酔的な

どのような平均であることは、私はすべての製品Iを知りたいときからそれらを取得する必要はありません、代わりにメモリ内のコレクション内のデータを格納する、であるDomain.ProductCollectionが、かどうか、尋ねると同じ、代わりにリポジトリを尋ねるDomain.ProductCollectionと製品が含まれています今回もSKUを渡し、代わりにリポジトリー(これは実際には例です)を要求します。これは、プリロードされた製品を反復する代わりに、基礎となるデータベースを照会します。必要がない限り、すべての製品をメモリに保存するつもりはありません。そうすることはまったくナンセンスです。
アンディ

どちらが別の質問につながりますか、リポジトリは属性が一意であるかどうかを知っている必要がありますか?私は常に、かなり単純なコンポーネントとしてリポジトリを実装し、保存するために渡すものを保存し、渡された条件に基づいてデータを取得し、決定をサービスに入れようとしています。
アンディ

@DavidPackerさて、「ドメインの知識」はインターフェースにあります。インターフェースは「ドメインにはこの機能が必要です」と言っており、データストアはその機能を提供します。とのIProductRepositoryインターフェースがある場合DoesNameExist、メソッドが何をすべきかは明らかです。ただし、既存の製品と同じ名前の製品を使用できないようにすることは、どこかのドメインにある必要があります。
陶酔

3

セットの検証については、Greg Youngを読む必要があります。

短い答え:ネズミの巣を深く掘り下げる前に、ビジネスの観点から要件の価値を確実に理解する必要があります。実際には、重複を防止するのではなく、検出して軽減するのにどれくらいの費用がかかりますか?

「一意性」要件の問題は、多くの場合、人々がそれを求めている根本的な理由があるということです- イヴ・レインハウト

より長い答え:私は可能性のメニューを見てきましたが、それらにはすべてトレードオフがあります。

コマンドをドメインに送信する前に、重複をチェックできます。これは、クライアントまたはサービスで行うことができます(例はテクニックを示しています)。ドメインレイヤーからロジックがリークすることに不満がある場合は、を使用して同じ種類の結果を得ることができますDomainService

class Product {
    void register(SKU sku, DuplicationService skuLookup) {
        if (skuLookup.isKnownSku(sku) {
            throw ProductWithSKUAlreadyExistsException(...)
        }
        ...
    }
}

もちろん、この方法で行うと、DeduplicationServiceの実装は、既存のskusを検索する方法について何かを知る必要があります。そのため、一部の作業がドメインに戻されますが、同じ基本的な問題(セットの検証に対する回答の必要性、競合状態の問題)に直面しています。

永続化レイヤー自体で検証を行うことができます。リレーショナルデータベースは、セットの検証に非常に優れています。製品のsku列に一意性制約を設定すれば、問題ありません。アプリケーションは製品をリポジトリに保存するだけで、問題がある場合は制約違反が発生します。したがって、アプリケーションコードは適切に見え、競合状態は解消されますが、「ドメイン」ルールが漏洩します。

既知のskusのセットを表す別の集計をドメインに作成できます。ここでは2つのバリエーションが考えられます。

1つはProductCatalogのようなものです。製品は別の場所に存在しますが、製品とskusの関係は、skuの一意性を保証するカタログによって維持されます。これは、製品にSKU がないことを意味するわけではありません。skusはProductCatalogによって割り当てられます(skusを一意にする必要がある場合は、単一のProductCatalog集計だけでこれを実現します)。ドメインの専門家とユビキタス言語を確認します。そのようなことが存在する場合、これは適切なアプローチである可能性があります。

別の方法は、SKU予約サービスのようなものです。基本的なメカニズムは同じです。集約はすべてのskusについて知っているため、重複の導入を防ぐことができます。ただし、メカニズムは少し異なります。製品に割り当てる前にSKUのリースを取得します。製品を作成するときに、リースをSKUに渡します。競合状態はまだありますが(集計が異なるため、トランザクションが異なります)、状況は異なります。ここでの本当の欠点は、ドメイン言語に正当な理由がなくてもリースサービスをドメインモデルに投影していることです。

すべての製品エンティティを単一の集合体(つまり、上記の製品カタログ)にプルできます。これを行うと、skusの一意性が確実に得られますが、コストは追加の競合であり、製品を変更すると、実際にはカタログ全体が変更されます。

インメモリで操作を行うためにデータベースからすべての製品SKUを引き出す必要がありません。

多分あなたはする必要はありません。Bloomフィルターを使用してSKU をテストすると、セットをまったく読み込まなくても、多くのユニークなSKUを発見できます。

ユースケースで、拒否するskusを任意に指定できる場合、すべての誤検知を取り除くことができます(コマンドを送信する前にクライアントが提案するskusをテストすることをクライアントに許可する場合は、大したことではありません)。これにより、セットをメモリに読み込まないようにすることができます。

(もっと受け入れたい場合は、ブルームフィルターで一致が発生した場合に、skusを遅延読み込みすることを検討できます。それでも、すべてのskusをメモリに読み込むリスクがある場合がありますが、許可する場合は一般的なケースではありません送信前にコマンドのエラーをチェックするクライアントコード)。


インメモリで操作を行うためにデータベースからすべての製品SKUを引き出す必要がありません。これは、最初の質問で提案し、そのパフォーマンスに疑問を投げかけた明白な解決策です。また、IMOは、一意性の責任を負うためにDBの制約に依存しているため、問題があります。新しいデータベースエンジンに切り替えたときに、変換中に一意制約が失われた場合は、以前存在していたデータベース情報がなくなっているため、コードが壊れています。とにかくリンクをありがとう、面白い読み。
アンディ

おそらくブルームフィルター(編集を参照)。
VoiceOfUnreason 2016年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.