web-dev-qa-db-ja.com

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

私はドメイン駆動設計を約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の原則について述べましたが、メソッド内では、手続き型のアプローチを使用してオブジェクトを操作しています。

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

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

10
Andy

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

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

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

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

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

7
Euphoric

set validation でGreg Youngを読む必要があります。

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

「一意性」要件の問題は、まあ、多くの場合、人々がそれを求めている根本的な理由があるということです-- Yves Reynhout

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

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

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

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

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

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

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

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

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

メモリ内で操作を行うために、データベースからすべての製品SKUを取得する必要がありません。

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

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

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

4
VoiceOfUnreason