Wzór specyfikacji w Domain Driven Design

Zmagałem się z problemem związanym z DDD ze specyfikacjami i czytałem wiele o DDD, specyfikacjach i repozytoriach.

Jednakże, istnieje problem, jeśli próbujesz połączyć wszystkie 3 z nich bez łamania domain driven design. Sprowadza się to do tego, jak stosować filtry z myślą o wydajności.

Najpierw kilka oczywistych faktów:

  1. repozytoria do warstwy got DataAccess / Infrastructure
  2. modele domen reprezentują logikę biznesową i przejdź do The Domain layer
  3. Modele dostępu do danych reprezentują warstwę Persistence i przejdź do warstwy Persistance/Infrastructure / DataAccess
  4. logika biznesowa przechodzi do warstwy domeny
  5. specyfikacje są logiką biznesową, więc należą również do warstwy domeny.
  6. W każdym z tych przykładów Framework ORM i SQL Server są używane wewnątrz repozytorium.]}
  7. modele trwałości nie mogą przeciekać do warstwy domeny

Jak dotąd, tak łatwo. Problem pojawia się, gdy / jeśli staramy się zastosuj specyfikacje do repozytorium i nie łamiąc wzorca DDD lub nie mając problemów z wydajnością.

Możliwe sposoby zastosowania specyfikacji:

1) klasyczny sposób: specyfikacje z wykorzystaniem modelu domeny w warstwie domeny

Zastosuj tradycyjny wzór specyfikacji, za pomocą metody IsSatisfiedBy, zwracając bool i specyfikacje złożone, aby połączyć wiele specyfikacji.

To pozwala nam zachować specyfikacje w warstwie domeny, ale...

  1. mA do pracy z modelami domeny, podczas gdy repozytorium używa modeli trwałości, które reprezentują strukturę danych warstwy trwałości. Ten jest łatwy do naprawienia za pomocą maperów, takich jak AutoMapper.
  2. jednak problem, którego nie można rozwiązać: wszystkie specyfikacje musiałyby być wykonywane w pamięci. W dużej tabeli / bazie danych oznacza to ogromny wpływ, jeśli musisz iterację przez wszystkie encje tylko po to, aby odfiltrować ten, który spełnia Twoje specyfikacje

2) Specyfikacje z wykorzystaniem modelu trwałości

Jest to podobne do 1), ale przy użyciu modeli trwałości w specyfikacji. Pozwala to na bezpośrednie użycie specyfikacji jako części naszego predykatu .Where, który zostanie przetłumaczony na zapytanie (tj. TSQL), a filtrowanie zostanie wykonane na pamięci masowej Persistence (tj. SQL Server).

  1. chociaż daje to dobrą wydajność ups, wyraźnie narusza wzór DDD. Nasz model trwałości przecieka do warstwy domeny, dzięki czemu Warstwa domeny zależy od warstwy trwałości, a nie na odwrót.

3) podobnie jak 2), ale uczyń specyfikacje częścią warstwy trwałości

  1. to nie działa, ponieważ warstwa domeny musi odwoływać się do specyfikacji. To nadal zależy od warstwy trwałości.
  2. mielibyśmy logikę biznesową wewnątrz warstwy trwałości. Który również narusza wzór DDD

4) Jak 3, ale użyj abstrakcyjnych specyfikacji jako Interfejsy

Mielibyśmy Interfejsy specyfikacji w naszej warstwie domeny, nasze konkretne implementacje specyfikacji w warstwie trwałości. Teraz nasza warstwa domeny oddziaływałaby tylko z interfejsami i nie zależałaby od warstwy trwałości.

  1. to nadal narusza #2 z 3). Mielibyśmy logikę biznesową w warstwie trwałości, co jest złe.

5) Przetłumacz drzewo wyrażeń z modelu domeny na trwałość Model

To z pewnością rozwiązuje problem, ale jest to nietrywialne zadanie, ale utrzymywałoby specyfikacje wewnątrz naszej warstwy domeny, a jednocześnie korzystałoby z optymalizacji SQL, ponieważ specyfikacje stają się częścią repozytoriów, w których klauzula i przekłada się na TSQL

Próbowałem tego podejścia i jest kilka problemów (strona implementacji formularza):

  1. musielibyśmy znać konfigurację z Mapera (jeśli go używamy) Lub zachować własną system mapowania. Można to częściowo zrobić (odczyt konfiguracji Mappera) za pomocą np. Automapp, ale istnieją dalsze problemy
  2. jest to dopuszczalne dla takiego, w którym jedna właściwość Modelu A odwzorowuje jedną właściwość modelu B. staje się to trudniejsze, jeśli typy są różne (np. ze względu na typy trwałości, na przykład liczby zapisywane jako łańcuchy lub pary klucz/wartość w innej tabeli i musimy wykonać konwersje wewnątrz resolvera.
  3. robi się to dość skomplikowane, jeśli wiele pól mapowane do jednego pola docelowego. Wydaje mi się, że nie jest to problem dla modelu domeny -> odwzorowania modelu Persistence

**6) Query Builder jak API **

Ostatnim z nich jest tworzenie pewnego rodzaju API zapytań, które jest przekazywane do specyfikacji i z którego warstwa repozytorium / Persistence generuje drzewo wyrażeń, które ma zostać przekazane do klauzuli .Where i które używa interfejsu do deklarowania wszystkich pól filtrujących.

Zrobiłem też kilka prób w tym kierunku, ale nie zbyt zadowolony z wyników. Coś jak

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; }
}

I w specyfikacji przekazalibyśmy IQuery<IPersonQueryFilter> query konstruktorowi specyfikacji, a następnie zastosowalibyśmy do niej specyfikacje podczas używania lub łączenia.

IQuery<IGridQueryFilter> query = null;

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

Nie podoba mi się to podejście, ponieważ sprawia, że obsługa złożonych specyfikacji jest nieco trudna (jak łańcuchowanie and lub if) I nie podoba mi się sposób, w jaki And/lub/Not działa, szczególnie tworzenie drzew wyrażeń z tego "API".

Szukałem weeks w całym Internecie czytają dziesiątki artykułów na temat DDD i specyfikacji, ale zawsze zajmują się tylko prostymi przypadkami i nie biorą pod uwagę wydajności lub naruszają wzór DDD.

Jak rozwiązać ten problem w rzeczywistych aplikacjach bez filtrowania pamięci lub przeciekania trwałości do warstwy domeny??

Czy istnieją frameworki, które rozwiązują powyższe problemy jednym z dwóch sposobów (Konstruktor zapytań, jak składnia do drzew wyrażeń lub Expression Tree translator)?

Author: Tseng, 2014-09-21

4 answers

Myślę, że wzorzec specyfikacji nie jest przeznaczony do kryteriów zapytań. Właściwie cała koncepcja DDD też nie jest. Rozważ CQRS, jeśli istnieje mnóstwo wymagań zapytań.

Wzór specyfikacji pomaga rozwinąć wszechobecny język, myślę, że to coś w rodzaju DSL. Deklaruje, co robić, a nie jak to robić. Na przykład w kontekście zamawiania zamówienia są uważane za zaległe, jeśli zostały złożone, ale nie zostały opłacone w ciągu 30 minut. Dzięki wzorcowi specyfikacji Twój zespół może rozmawiać z krótkim, ale wyjątkowym terminem: OverdueOrderSpecification. Wyobraź sobie dyskusję poniżej:

Case -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..

Case -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....
Który wolisz?

Zwykle potrzebujemy DSL handler do analizy dsl, w tym przypadku może to być w adapterze persistence, tłumaczy specyfikację na kryteria zapytania. Zależność ta (właśc.persistence => domain) nie narusza zasad architektury.

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 ...
    }
}
 4
Author: Hippoom,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-22 15:06:53

Kiedyś zaimplementowałem specyfikację, ale...

    Bazował na LINQ i IQueryable.
  1. używało pojedynczego, zunifikowanego repozytorium (ale jak dla mnie nie jest źle i myślę, że to główny powód do używania specyfikacji).
  2. używał jednego modelu dla domeny i stałych potrzeb (co uważam za złe).

Repozytorium:

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
}

Specyfikacja filtra:

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);
    }
}

Inny rodzaj specyfikacji:

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);
    }

}

I zastosowania:

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

Custom implementacje mogą wyglądać tak:

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);
    }
}

Wreszcie, jestem zmuszony powiedzieć, że prosta Specficiation implementacja źle pasuje do DDD. Zrobiłeś świetne badania w tej dziedzinie i jest mało prawdopodobne, że ktoś zaproponuje coś nowego :). Zobacz też: http://www.sapiensworks.com/blog / blog.

 4
Author: Valentin P.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-11 15:24:50
[[2]] szukałem od tygodni w całym Internecie, przeczytałem dziesiątki artykuły na temat DDD i specyfikacji, ale zawsze obsługują tylko proste przypadków i nie biorą pod uwagę wydajności lub ich naruszać wzorzec DDD.

Ktoś mnie poprawi, jeśli się mylę, ale wydaje mi się, że pojęcie "modelu Persistence" pojawiło się dopiero niedawno w przestrzeni DDD (swoją drogą, gdzie o tym czytałeś ?). Nie jestem pewien, czy jest to opisane w oryginalna niebieska Księga.

Ja osobiście nie widzę w tym wielu zalet. Moim zdaniem masz (zazwyczaj) model relacyjny w bazie danych i model domeny w pamięci w aplikacji. Luka między nimi jest wypełniana przez działanie, a nie model. Działanie to może być wykonywane przez ORM. Mam jeszcze do sprzedania fakt, że "persistence object model" naprawdę ma sens semantycznie, nie mówiąc już o obowiązku przestrzegania zasad DDD (*).

Teraz jest podejście CQRS, gdzie masz oddzielny model odczytu, ale jest to zupełnie inne zwierzę i nie widzę Specifications działania na obiektach modelu odczytu zamiast encji jako naruszenia DDD w tym przypadku. Specyfikacja jest wszakże bardzo ogólnym wzorcem, którego nic w DDD zasadniczo nie ogranicza do Bytów.

(*) Edit : twórca Automapp Jimmy Bogard wydaje się być zbyt skomplikowany - Zobacz Jak używać automapp do mapowania wielu do wielu związki?

 1
Author: guillaume31,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:26:17

Spóźnię się na imprezę, bug tu są moje 2 centy...

Zmagałem się również z implementacją wzoru specyfikacji z dokładnie tych samych powodów, które opisałeś powyżej. Jeśli zrezygnujesz z wymogu oddzielnego modelu (trwałość / domena), twój problem zostanie znacznie uproszczony. możesz dodać inną metodę do specyfikacji, aby wygenerować drzewo wyrażeń dla ORM:

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

Jest post z Valdmira Khorikova opisujący jak to zrobić w szczegóły.

Jednakże, naprawdę nie lubię mieć jednego modelu . Podobnie jak ty uważam, że model Perystencji powinien być utrzymywany w warstwie infrastruktury, aby nie zanieczyszczać Twojej domeny z powodu ograniczeń ORM.

W końcu wymyśliłem rozwiązanie, używając gościa, aby przetłumaczyć model domeny na drzewo wyrażeń modelu persistence.

Ostatnio napisałem serię postów, w których wyjaśniam

Efekt końcowy staje się bardzo prosty w rzeczywistości, musisz zrobić specyfikację Visitable...

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

Utwórz SpecificationVisitor aby przetłumaczyć specyfikację na wyrażenie:

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
}

Jest tylko tweeking, który trzeba zrobić, jeśli chcesz stworzyć ogólne spefication. Jego wszystkie szczegółowe w postach wymienionych powyżej.

 1
Author: Fabio Marreco,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-09 21:46:39