可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I've been struggling around an DDD related issue with Specifications and I've read much into DDD and specifications and repositories.
However, there is an issue if trying to combine all 3 of these without breaking the domain driven design. It boils down to how to apply filters with performance in mind.
First a few obvious facts:
- Repositories to got DataAccess/Infrastructure Layer
- Domain Models represent Business Logic and go to the Domain layer
- Data Access Models represent Persistence layer and go to the Persistance/Infrastructure/DataAccess layer
- Business Logic goes to Domain Layer
- Specifications are Business Logic, so they belong to Domain layer too.
- In all this examples, an ORM Framework and SQL Server is used inside the Repository
- Persistance Models may not leak into Domain Layer
So far, so easy. The problem arises when/if we try to apply Specifications to the Repository and not breaking DDD pattern or having performance issues.
The possible ways to apply Specifications:
1) Classic way: Specifications using Domain Model in Domain Layer
Apply the traditional Specification Pattern, with a IsSatisfiedBy
method, returning a bool
and Composite Specifications to combine multiple Specifications.
This let us keep specifications in Domain Layer, but...
- It has to work with Domain Models, while the repository uses Persistence Models which represent the data structure of the persistence layer. This one is easy to fix with usage of mappers such as
AutoMapper
.
- However, the problem which can't be solved: All the specifications would have to be performed in memory. In a big table/database this means a huge impact if you have to iterate through ALL Entities only to filter out the one which meet your specifications
2) Specifications using Persistence Model
This is similar to 1), but using Persistence Models in the specification. This allows directly use the Specification as part of our .Where
predicate which will be translated into a query (i.e. TSQL) and the filtering will be performed on the Persistence storage (i.e. SQL Server).
- While this gives ups good performance, it clearly violates the DDD pattern. Our Persistence model leak into the Domain layer, making the Domain Layer depend on Persistence Layer instead of the other way around.
3) Like 2), but make Specifications Part of the Persistence Layer
- This doesn't work, because Domain Layer needs to reference the Specifications. It would still depend on persistence layer.
- We would have business logic inside Persistence layer. Which also violates DDD pattern
4) Like 3, but use abstract the Specifications as Interfaces
We would have Specification interfaces in our Domain layer, our concrete implementations of the Specifications in the Persistence Layer. Now our Domain Layer would only interact with the interfaces and not depend on the Persistence layer.
- This still violates #2 from 3). We would have business logic in persistence layer, which is bad.
5) Translate the Expression Tree from Domain Model into Persistence Model
This certainly solves the problem, but it's non-trivial task but it would keep the Specifications inside our Domain Layer while still benefiting from SQL optimization, because the Specifications becomes part of the Repositories Where clause and translates into TSQL
I tried going this approach and there are several issues (form implementation side):
- We would need to know the Configuration from the Mapper (if we use one) or keep our own mapping system. This can be partly done (reading Mapper configuration) with i.e. AutoMapper, but further issues exist
- It's acceptable for one where one Property of Model A maps to one Property of Model B. It becomes more difficult if the types are different (i.e. due to persistence types, for example Enums being saved as strings or key/value pairs in another table and we need to do conversions inside the resolver.
- It gets pretty complicated, if multiple fields get mapped into one destination field. I believe this is non an issue for Domain Model -> Persistence Model mappings
**6) Query Builder like API **
The last one is making some kind of query API which is passed into the specification and from whom the Repository/Persistence layer would generate an Expression Tree to be passed to .Where
clause and which uses an Interface to declare all filterable fields.
I did a few attempts in that direction too, but wasn't too happy about the results. Something like
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; }
}
and in the specification we would pass a IQuery<IPersonQueryFilter> query
to the specifications constructor and then apply the specifications to it when using or combining it.
IQuery<IGridQueryFilter> query = null;
query.Where(f => f.Name.Equal("Bob") );
I don't like this approach much, as it makes handling complex specifications somewhat hard (like and or if chaining) and I don't like the way the And/Or/Not would work, especially creating expression trees from this "API".
I have been looking for weeks all over the Internet, read dozens of articles on DDD and Specification, but they always only handle simple cases and don't take the performance into consideration or they violate DDD pattern.
How do you solve this in a real world application without doing in memory filtering or leaking Persistence into Domain Layer??
Are there any frameworks which solve issues above with one of the two ways (Query Builder like syntax to Expression Trees or an Expression Tree translator)?
回答1:
I think Specification pattern is not designed for query criteria. Actually, the whole concept of DDD is not, either. Consider CQRS if there are plethora of query requirements.
Specification pattern helps develop ubiquitous language, I think it's like kind of a DSL. It declares what to do rather than how to do it. For example, in a ordering context, orders are considered as overdue if it was placed but not paid within 30 minutes. With Specification pattern, your team can talk with a short but unique term: OverdueOrderSpecification. Imagine the discussion below:
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....
Which one do you prefer?
Usually, we need a DSL handler to parse the dsl, in this case, it may be in the persistence adapter, translates the specification to a query criteria. This dependence (infrastrructure.persistence => domain) does not violates the architecture principal.
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 ...
}
}
回答2:
Once I implemented Specification but...
- It was based on LINQ and IQueryable.
- It used single unified Repository (but as for me it's not bad and I think that it's main reason to use Specification).
- It used single model for domain and persistant needs (which I think to be bad).
Repository:
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
}
Filter specification:
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);
}
}
Another kind of specification:
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);
}
}
and usages:
OrderRepository.Compute(new MaxInvoiceNumberComputationSpecification()) + 1
PlaceRepository.Single(FilterSpecification<Place>.CreateByPredicate(p => p.Name == placeName));
UnitRepository.Compute(new UnitsAreAvailableForPickingFilterSpecification() & new CheckStockContainsEnoughUnitsOfGivenProductComputatonSpecification(count, product));
Custom implementations may look like
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);
}
}
Finally, I'm forced to tell that simple Specficiation
implementation fits bad according to DDD. You have done great research in this area and it's unlikely that someone proposes something new :). Also, take a look at http://www.sapiensworks.com/blog/ blog.
回答3:
I have been looking for weeks all over the Internet, read dozens of
articles on DDD and Specification, but they always only handle simple
cases and don't take the performance into consideration or they
violate DDD pattern.
Someone will correct me if I'm wrong, but it seems to me that the concept of a "Persistence Model" didn't appear until very recently in the DDD space (by the way, where did you read about it ?). I'm not sure it's described in the original blue book.
I personally don't see many advantages to it. My view is that you have a persisted (usually) relational model in your database and an in-memory domain model in your application. The gap between the two is bridged by an action, not a model. This action can be performed by an ORM. I have yet to be sold on the fact that a "Persistence object model" really makes sense semantically, let alone is mandatory to respect DDD principles (*).
Now there's the CQRS approach where you have a separate Read Model, but this is a totally different animal and I wouldn't see Specifications
acting on Read Model objects instead of Entities as a DDD violation in this case. Specification is after all a very general pattern that nothing in DDD fundamentally restricts to Entities.
(*) Edit : Automapper creator Jimmy Bogard seems to find it overcomplicated as well - See How do I use automapper to map many-to-many relationships?
回答4:
I´m late for the party, bug here are my 2 cents...
I did also struggle implementing the specification pattern for exactly the same reasons you described above. If you abandon the requirement for a separate model (Persistence / Domain) then your problem is greatly simplified. you could add another method to the specification to generate the expression tree for the ORM:
public interface ISpecification<T>
{
bool IsSpecifiedBy(T item);
Expression<Func<T, bool>> GetPredicate()
}
There is a post from Valdmir Khorikov describing how to do that in detail.
However, I really don´t like have a single model. Like you I find that Peristence model should be kept in the infrastructure layer to not contaminate your domain because of ORM limitations.
Eventually I came up with a solution using a visitor to translate the the domain model to a expression tree of the persistence model.
I recently wrote a series of posts where I explain
- How to create a Generic specification in C#
- What is a Visitor Design Pattern and how to make it generic.
- And my take on how to implement specification pattern in entity framework
The end result to use becomes very simple actually, you´ll need to make the specification Visitable...
public interface IProductSpecification
{
bool IsSpecifiedBy(Product item);
TResult Accept(IProductSpecificationVisitor<TResult> visitor);
}
Create a SpecificationVisitor
to translate the specification to a expression:
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
}
There is just some tweeking that needs to be done if you want to create a generic spefication. It´s all detailed in the posts referenced above.