web-dev-qa-db-ja.com

ドメイン駆動設計の仕様パターン

私は仕様に関するDDD関連の問題に苦労してきましたが、DDDと仕様およびリポジトリについて多くのことを読みました。

ただし、ドメイン駆動型の設計を壊さずにこれら3つすべてを組み合わせようとすると、問題が発生します。つまり、パフォーマンスを念頭に置いてフィルターを適用する方法です。

最初のいくつかの明らかな事実:

  1. DataAccess /インフラストラクチャレイヤーを取得するためのリポジトリ
  2. ドメインモデルはビジネスロジックを表し、ドメインレイヤーに移動します
  3. データアクセスモデルは永続性レイヤーを表し、永続性/インフラストラクチャ/データアクセスレイヤーに移動します
  4. ビジネスロジックはドメインレイヤーに移行します
  5. 仕様はビジネスロジックであるため、ドメイン層にも属します。
  6. これらすべての例では、ORMフレームワークとSQLServerがリポジトリ内で使用されています
  7. 永続性モデルはドメインレイヤーにリークしない可能性があります

これまでのところ、とても簡単です。この問題は、仕様をリポジトリに適用しようとして、DDDパターンを壊したり、パフォーマンスの問題が発生したりしない場合に発生します。

仕様を適用するための可能な方法:

1)古典的な方法:ドメインレイヤーでドメインモデルを使用する仕様

IsSatisfiedByメソッドを使用して従来の仕様パターンを適用し、boolと複合仕様を返して、複数の仕様を組み合わせます。

これにより、仕様をドメインレイヤーに保持できますが...

  1. リポジトリは永続性レイヤーのデータ構造を表す永続性モデルを使用しますが、ドメインモデルと連携する必要があります。これは、AutoMapperなどのマッパーを使用すると簡単に修正できます。
  2. ただし、解決できない問題:すべての仕様をメモリ内で実行する必要があります。大きなテーブル/データベースでは、仕様を満たすエンティティを除外するためにすべてのエンティティを反復処理する必要がある場合、これは大きな影響を意味します

2)永続性モデルを使用した仕様

これは1)に似ていますが、仕様で永続性モデルを使用しています。これにより、.Where述語の一部として仕様を直接使用できます。この述語はクエリ(つまりTSQL)に変換され、フィルタリングは永続ストレージ(つまり、SQL Server)で実行されます。

  1. これにより良好なパフォーマンスが得られますが、明らかにDDDパターンに違反しています。私たちの永続性モデルはドメインレイヤーにリークし、ドメインレイヤーをその逆ではなく永続性レイヤーに依存させます。

)2)と同様ですが、仕様を永続層の一部にします

  1. ドメインレイヤーは仕様を参照する必要があるため、これは機能しません。それでも永続層に依存します。
  2. 永続層内にビジネスロジックがあります。これもDDDパターンに違反します

4)3と同様ですが、インターフェイスとして仕様を抽象化して使用します

ドメイン層に仕様インターフェースがあり、永続層に仕様の具体的な実装があります。これで、ドメインレイヤーはインターフェースとのみ対話し、永続レイヤーには依存しなくなります。

  1. これはまだ3)の#2に違反しています。永続層にビジネスロジックがありますが、これは悪いことです。

5)式ツリーをドメインモデルから永続性モデルに変換します

これは確かに問題を解決しますが、それは簡単な作業ではありませんが、仕様はRepositories Where句の一部になり、TSQLに変換されるため、SQL最適化の恩恵を受けながら、仕様をドメインレイヤー内に保持します。

このアプローチを試してみましたが、いくつかの問題があります(フォームの実装側)。

  1. マッパーから構成を知るか(使用する場合)、独自のマッピングシステムを維持する必要があります。これは、AutoMapperを使用して部分的に実行できます(Mapper構成の読み取り)が、さらに問題が存在します
  2. モデルAの1つのプロパティがモデルBの1つのプロパティにマップされる場合は許容されます。タイプが異なる場合(つまり、列挙型が文字列またはキーと値のペアとして別のテーブルに保存されているなどの永続性タイプのため)、リゾルバー内で変換を行う必要があります。
  3. 複数のフィールドが1つの宛先フィールドにマップされると、かなり複雑になります。これはドメインモデル->永続性モデルのマッピングでは問題ではないと思います

6)APIのようなクエリビルダー

最後の1つは、仕様に渡され、リポジトリ/永続層が.Where句に渡される式ツリーを生成し、インターフェイスを使用してすべてのフィルタリング可能なフィールドを宣言する、ある種のクエリAPIを作成することです。

私もその方向に何度か試みましたが、結果にはあまり満足していませんでした。何かのようなもの

public interface IQuery<T>
{
    IQuery<T> Where(Expression<Func<T, T>> predicate);
}
public interface IQueryFilter<TFilter>
{
    TFilter And(TFilter other);
    TFilter Or(TFilter other);
    TFilter Not(TFilter other);
}

public interface IQueryField<TSource, IQueryFilter>
{
    IQueryFilter Equal(TSource other);
    IQueryFilter GreaterThan(TSource other);
    IQueryFilter Greater(TSource other);
    IQueryFilter LesserThan(TSource other);
    IQueryFilter Lesser(TSource other);
}
public interface IPersonQueryFilter : IQueryFilter<IPersonQueryFilter>
{
    IQueryField<int, IPersonQueryFilter> ID { get; }
    IQueryField<string, IPersonQueryFilter> Name { get; }
    IQueryField<int, IPersonQueryFilter> Age { get; }
}

仕様では、IQuery<IPersonQueryFilter> queryを仕様コンストラクターに渡し、それを使用または組み合わせるときに仕様を適用します。

IQuery<IGridQueryFilter> query = null;

query.Where(f => f.Name.Equal("Bob") );

このアプローチは、複雑な仕様の処理がやや難しくなり(チェーンの場合など)、And/Or/Notの動作方法、特にこの「API」から式ツリーを作成する方法が気に入らないため、あまり好きではありません。 。

私はインターネット全体でweeksを探していて、DDDと仕様に関する数十の記事を読んでいますが、それらは常に単純なケースのみを扱い、パフォーマンスを考慮していないか、DDDパターンに違反しています。

メモリフィルタリングを実行したり、ドメインレイヤーに永続性をリークしたりせずに、実際のアプリケーションでこれをどのように解決しますか?

2つの方法(式ツリーへの構文のようなクエリビルダーまたは式ツリートランスレーター)のいずれかで上記の問題を解決するフレームワークはありますか?

24
Tseng

仕様パターンはクエリ条件用に設計されていないと思います。実際、DDDの概念全体もそうではありません。クエリ要件が多すぎる場合は、CQRSを検討してください。

仕様パターンはユビキタス言語の開発に役立ちます。DSLのようなものだと思います。それは、それをどのように行うかではなく、何をすべきかを宣言します。たとえば、注文のコンテキストでは、注文が行われたが30分以内に支払われなかった場合、注文は期限切れと見なされます。仕様パターンを使用すると、チームは短いが一意の用語であるOverdueOrderSpecificationと話すことができます。以下の議論を想像してみてください。

ケース-1

Business people: I want to find out all overdue orders and ...  
Developer: I can do that, it is easy to find all satisfying orders with an overdue order specification and..

ケース-2

Business people: I want to find out all orders which were placed before 30 minutes and still unpaid...  
Developer: I can do that, it is easy to filter order from tbl_order where placed_at is less that 30minutes before sysdate....

あなたはどちらを好みますか?

通常、dslを解析するためにDSLハンドラーが必要です。この場合、DSLハンドラーは永続アダプター内にある可能性があり、仕様をクエリ基準に変換します。この依存関係(infrastrructure.persistence =>ドメイン)は、アーキテクチャープリンシパルに違反しません。

class OrderMonitorApplication {
    public void alarm() {
       // The specification pattern keeps the overdue order ubiquitous language in domain
       List<Order> overdueOrders = orderRepository.findBy(new OverdueSpecification());
       for (Order order: overdueOrders) {
           //notify admin
       }
    }
}

class HibernateOrderRepository implements orderRepository {
    public List<Order> findBy(OrderSpecification spec) {
        criteria.le("whenPlaced", spec.placedBefore())//returns sysdate - 30
        criteria.eq("status", spec.status());//returns WAIT_PAYMENT
        return ...
    }
}
8
Yugang Zhou

仕様を実装したら...

  1. これは、LINQとIQueryableに基づいていました。
  2. 単一の統合リポジトリを使用していました(しかし、私にとっては悪くはなく、仕様を使用する主な理由だと思います)。
  3. ドメインと永続的なニーズに単一のモデルを使用しました(これは悪いと思います)。

リポジトリ:

public interface IRepository<TEntity> where TEntity : Entity, IAggregateRoot
{
    TEntity Get<TKey>(TKey id);

    TEntity TryGet<TKey>(TKey id);

    void DeleteByKey<TKey>(TKey id);

    void Delete(TEntity entity);

    void Delete(IEnumerable<TEntity> entities);

    IEnumerable<TEntity> List(FilterSpecification<TEntity> specification);

    TEntity Single(FilterSpecification<TEntity> specification);        

    TEntity First(FilterSpecification<TEntity> specification);

    TResult Compute<TResult>(ComputationSpecification<TEntity, TResult> specification);

    IEnumerable<TEntity> ListAll();

    //and some other methods
}

フィルター仕様:

public abstract class FilterSpecification<TAggregateRoot> where TAggregateRoot : Entity, IAggregateRoot
{

     public abstract IQueryable<TAggregateRoot> Filter(IQueryable<TAggregateRoot> aggregateRoots);

     public static FilterSpecification<TAggregateRoot> CreateByPredicate(Expression<Func<TAggregateRoot, bool>> predicate)
     {
         return new PredicateFilterSpecification<TAggregateRoot>(predicate);
     }      

     public static FilterSpecification<TAggregateRoot> operator &(FilterSpecification<TAggregateRoot> op1, FilterSpecification<TAggregateRoot> op2)
     {
         return new CompositeFilterSpecification<TAggregateRoot>(op1, op2);
     }        

     public static FilterSpecification<TAggregateRoot> CreateDummy()
     {
         return new DummyFilterSpecification<TAggregateRoot>();
     }

}


public class CompositeFilterSpecification<TAggregateRoot> : FilterSpecification<TAggregateRoot> where TAggregateRoot : Entity, IAggregateRoot
{

    private readonly FilterSpecification<TAggregateRoot> _firstOperand;
    private readonly FilterSpecification<TAggregateRoot> _secondOperand;

    public CompositeFilterSpecification(FilterSpecification<TAggregateRoot> firstOperand, FilterSpecification<TAggregateRoot> secondOperand)
    {
        _firstOperand = firstOperand;
        _secondOperand = secondOperand;
    }

    public override IQueryable<TAggregateRoot> Filter(IQueryable<TAggregateRoot> aggregateRoots)
    {
        var operand1Results = _firstOperand.Filter(aggregateRoots);
        return _secondOperand.Filter(operand1Results);
    }
}

public class PredicateFilterSpecification<TAggregateRoot> : FilterSpecification<TAggregateRoot> where TAggregateRoot : Entity, IAggregateRoot
{

    private readonly Expression<Func<TAggregateRoot, bool>> _predicate;

    public PredicateFilterSpecification(Expression<Func<TAggregateRoot, bool>> predicate)
    {
        _predicate = predicate;
    }

    public override IQueryable<TAggregateRoot> Filter(IQueryable<TAggregateRoot> aggregateRoots)
    {
        return aggregateRoots.Where(_predicate);
    }
}

別の種類の仕様:

public abstract class ComputationSpecification<TAggregateRoot, TResult> where TAggregateRoot : Entity, IAggregateRoot
{

    public abstract TResult Compute(IQueryable<TAggregateRoot> aggregateRoots);

    public static CompositeComputationSpecification<TAggregateRoot, TResult> operator &(FilterSpecification<TAggregateRoot> op1, ComputationSpecification<TAggregateRoot, TResult> op2)
    {
        return new CompositeComputationSpecification<TAggregateRoot, TResult>(op1, op2);
    }

}

および使用法:

OrderRepository.Compute(new MaxInvoiceNumberComputationSpecification()) + 1
PlaceRepository.Single(FilterSpecification<Place>.CreateByPredicate(p => p.Name == placeName));
UnitRepository.Compute(new UnitsAreAvailableForPickingFilterSpecification() & new CheckStockContainsEnoughUnitsOfGivenProductComputatonSpecification(count, product));

カスタム実装は次のようになります

public class CheckUnitsOfGivenProductExistOnPlaceComputationSpecification : ComputationSpecification<Unit, bool>
{
    private readonly Product _product;
    private readonly Place _place;

    public CheckUnitsOfGivenProductExistOnPlaceComputationSpecification(
        Place place,
        Product product)
    {
        _place = place;
        _product = product;
    }

    public override bool Compute(IQueryable<Unit> aggregateRoots)
    {
        return aggregateRoots.Any(unit => unit.Product == _product && unit.Place == _place);
    }
}

最後に、DDDによれば、単純なSpecficiationの実装は適切ではないと言わざるを得ません。あなたはこの分野で素晴らしい研究をしてきましたが、誰かが何か新しいことを提案する可能性は低いです:)。また、 http://www.sapiensworks.com/blog/ ブログもご覧ください。

5
Valentin P.

私はパーティーに遅れています、ここのバグは私の2セントです...

また、上記とまったく同じ理由で、仕様パターンの実装にも苦労しました。別のモデル(永続性/ドメイン)の要件を放棄すると、問題は大幅に単純化されます。仕様に別のメソッドを追加して、ORMの式ツリーを生成できます。

public interface ISpecification<T>
{
    bool IsSpecifiedBy(T item);
    Expression<Func<T, bool>> GetPredicate()
}

Valdmir Khorikovからの投稿 それを詳細に行う方法を説明しています。

ただし、単一のモデルは本当に好きではありません。あなたと同じように、ORMの制限のためにドメインを汚染しないように、永続性モデルをインフラストラクチャ層に保持する必要があることがわかりました。

最終的に、訪問者を使用してドメインモデルを永続性モデルの式ツリーに変換するソリューションを思いつきました。

私は最近、私が説明する一連の投稿を書きました

最終結果は実際に使用するのが非常に簡単になります。仕様を作成する必要がありますVisitable...

public interface IProductSpecification
{
    bool IsSpecifiedBy(Product item);
    TResult Accept<TResult>(IProductSpecificationVisitor<TResult> visitor);
}

SpecificationVisitorを作成して、仕様を式に変換します。

public class ProductEFExpressionVisitor : IProductSpecificationVisitor<Expression<Func<EFProduct, bool>>> 
{
    public Expression<Func<EFProduct, bool>> Visit (ProductMatchesCategory spec) 
    {
        var categoryName = spec.Category.CategoryName;
        return ef => ef.Category == categoryName;
    }

    //other specification-specific visit methods
}

一般的なスペシフィケーションを作成する場合は、実行する必要のあるいくつかの調整があります。それはすべて、上記の投稿で詳しく説明されています。

2
Fabio Marreco

私はインターネット全体で何週間も探していて、DDDと仕様に関する数十の記事を読んでいますが、それらは常に単純なケースのみを扱い、パフォーマンスを考慮していないか、DDDパターンに違反しています。

私が間違っていれば誰かが私を訂正してくれるでしょうが、「永続性モデル」の概念はごく最近までDDDスペースに現れていなかったようです(ところで、どこでそれについて読んだのですか?)。元の青い本に記載されているかどうかはわかりません。

個人的にはあまりメリットはありません。私の見解では、データベースにはpersisted(通常)リレーショナルモデルがあり、アプリケーションにはメモリ内ドメインモデルがあります。 2つの間のギャップは、モデルではなくactionによって埋められます。このアクションは、ORMによって実行できます。 「永続オブジェクトモデル」が本当に意味的に意味があるという事実については、まだ販売されていません。ましてや、DDDの原則を尊重することは必須です(*)。

個別の読み取りモデルがあるCQRSアプローチがありますが、これはまったく異なる動物であり、この場合、エンティティではなく読み取りモデルオブジェクトにSpecificationsが作用することはありません。結局のところ、仕様は非常に一般的なパターンであり、DDDでは基本的にエンティティに制限されるものはありません。

(*)編集:オートマッパーの作成者であるジミー・ボガードも、それが複雑すぎると感じているようです オートマッパーを使用して多対多の関係をマッピングするにはどうすればよいですか?

1
guillaume31