在DDD中,存储库应公开实体还是域对象?


11

据我了解,在DDD中,将存储库模式与聚合根一起使用是合适的。我的问题是,是否应将数据作为实体或域对象/ DTO返回?

也许一些代码可以进一步解释我的问题:

实体

public class Customer
{
  public Guid Id { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
}

我应该做这样的事情吗?

public Customer GetCustomerByName(string name) { /*some code*/ }

还是类似的东西?

public class CustomerDTO
{
  public Guid Id { get; set; }
  public FullName { get; set; }
}

public CustomerDTO GetCustomerByName(string name) { /*some code*/ }

附加问题:

  1. 在存储库中,我应该返回IQueryable还是IEnumerable?
  2. 在服务或资料库,我应该这样做.. GetCustomerByLastNameGetCustomerByFirstNameGetCustomerByEmail?或只是做一个类似的方法GetCustomerBy(Func<string, bool> predicate)

GetCustomerByName('John Smith')如果您的数据库中有二十个John Smiths,将返回什么?您似乎假设没有两个人具有相同的名字。
bdsl

Answers:


8

我应该将数据作为实体或域对象/ DTO返回

好吧,这完全取决于您的用例。我可以考虑返回DTO而不是完整实体的唯一原因是,如果您的实体很大,而您只需要处理它的一个子集。

如果是这种情况,那么也许您应该重新考虑您的域模型,并将大实体拆分为相关的较小实体。

  1. 在存储库中,我应该返回IQueryable还是IEnumerable?

一个好的经验法则是始终返回最简单的类型(在继承层次结构中最高)。因此,IEnumerable除非您希望让存储库使用者使用,否则请返回IQueryable

就个人而言,我认为返回an IQueryable是一个泄漏的抽象,但是我遇到了一些开发人员,他们热情地认为并非如此。在我看来,所有查询逻辑都应由存储库包含和隐藏。如果允许调用代码自定义他们的查询,那么存储库的意义何在?

  1. 在服务或存储库中,我应该做类似的事情吗。GetCustomerByLastName,GetCustomerByFirstName,GetCustomerByEmail?还是只是创建类似于GetCustomerBy(Func predicate)的方法?

出于与我在第1点中提到的相同的原因,绝对不要使用GetCustomerBy(Func<string, bool> predicate)。乍一看似乎很诱人,但这正是人们学会讨厌通用存储库的原因。漏了

诸如此类GetByPredicate(Func<T, bool> predicate)的东西仅在隐藏在具体类后面时才有用。因此,如果您有一个称为的RepositoryBase<T>暴露的抽象基类,而protected T GetByPredicate(Func<T, bool> predicate)该基类仅由具体的存储库(例如public class CustomerRepository : RepositoryBase<Customer>)使用,则可以。


因此,您的说法是,在域层中具有DTO类可以吗?
codefish

那不是我想说的。我不是DDD专家,所以我不能说这是否可以接受。出于好奇,您为什么不不退还全部实体?太大了吗
MetaFight

哦,不是。我只想知道什么是正确和可以接受的事情。返回完整实体还是仅返回其子集。我想这取决于您的答案。
codefish

6

有相当一部分人使用CQRS来实现其域。我的感觉是,如果存储库的界面类似于它们使用的最佳实践,那么您就不会误入歧途。

根据我所见...

1)命令处理程序通常使用存储库通过存储库加载聚合。命令针对集合的单个特定实例;存储库通过ID加载根。我看不到有一种针对集合集合运行命令的情况(相反,您将首先运行查询以获取集合集合,然后枚举集合并对每个集合发出命令。

因此,在要修改聚合的情况下,我希望存储库返回实体(也就是聚合根)。

2)查询处理程序完全不涉及聚合;取而代之的是,它们使用聚合的投影进行工作-值对象,这些对象描述某个时间点聚合的状态。因此,以ProjectionDTO而不是AggregateDTO为例,您将拥有正确的想法。

在要针对聚合运行查询,准备进行显示等操作的情况下,我希望看到返回的是DTO或DTO集合,而不是实体。

您的所有getCustomerByProperty来电对我来说都像是查询,因此它们属于后一类。我可能想使用一个入口点来生成集合,所以我想看看是否

getCustomersThatSatisfy(Specification spec)

是一个合理的选择;然后查询处理程序将根据给定的参数构造适当的规范,并将该规范传递到存储库。缺点是签名确实表明存储库是内存中的集合。我不清楚,如果存储库只是对关系数据库运行SQL语句的抽象,则谓词会为您带来很多好处。

不过,有些模式可以提供帮助。例如,与其手动构建规范,不如将约束的描述传递给存储库,并允许存储库的实现决定要做什么。

警告:检测到类似Java的输入

interface CustomerRepository {
    interface ConstraintBuilder {
        void setLastName();
        void setFirstName();
    }

    interface ConstraintDescriptor {
        void copyTo(ConstraintBuilder builder);
    }

    List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor);
}

SQLBackedCustomerRepository implements CustomerRepository {
    List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor) {
        WhereClauseBuilder builder = new WhereClauseBuilder();
        descriptor.copyTo(builder);
        Query q = createQuery(builder.build());
        //...
     }
}

CollectionBackedCustomerRepository implements CustomerRepository {
    List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor) {
        PredicateBuilder builder = new PredicateBuilder();
        descriptor.copyTo(builder);
        Predicate p = builder.build();
        // ...
}

class MatchLastName implements CustomerRepository.ConstraintDescriptor {
    private final lastName;
    // ...

    void copyTo(CustomerRepository.ConstraintBuilder builder) {
        builder.setLastName(this.lastName);
    }
}

总结:提供汇总和提供DTO之间的选择取决于您期望消费者如何使用它。我的猜测是支持每个上下文接口的一种具体实现。


一切都很好,但是询问者没有提到使用CQRS。没错,如果他做到了,他的问题将不复存在。
MetaFight

我对CQRS一无所知。据我了解,当考虑使用AggregateDTO或ProjectionDTO返回什么时,我将返回ProjectionDTO。然后在上,getCustomersThatSatisfy(Specification spec)我将仅列出搜索选项所需的属性。我说对了吗?
codefish

不清楚 我不认为AggregateDTO应该存在。汇总的目的是确保所有更改都满足业务不变性。它是需要满足的域规则的封装-即,它是行为。另一方面,投影是业务可以接受的状态快照的表示。请参阅编辑以尝试阐明规范。
VoiceOfUnreason

我同意VoiceOfUnreason的观点,认为AggregateDTO不应该存在-我认为ProjectionDTO仅是数据的视图模型。它可以根据呼叫者的需求调整其形状,并根据需要从各种来源提取数据。“聚合根”应该是所有相关数据的完整表示,以便可以在保存之前测试所有引用规则。它仅在更剧烈的情况下才会更改形状,例如DB表更改或修改的数据关系。
Brad Irby

1

根据我对DDD的了解,您应该有这个,

public CustomerDTO GetCustomerByName(string name) { /*some code*/ }

在存储库中,我应该返回IQueryable还是IEnumerable?

这取决于您是否会发现来自不同民族的混合意见。但是我个人认为,如果您的存储库将由CustomerService之类的服务使用,则可以使用IQueryable,否则请坚持使用IEnumerable。

存储库应该返回IQueryable吗?

在服务或存储库中,我应该做类似的事情吗。GetCustomerByLastName,GetCustomerByFirstName,GetCustomerByEmail?还是只是创建类似于GetCustomerBy(Func predicate)的方法?

在您的存储库中,您应该像您说的那样具有通用功能,GetCustomer(Func predicate)但是在服务层中添加三种不同的方法,这是因为有可能从不同的客户端调用您的服务,并且它们需要不同的DTO。

如果您还不知道,也可以对通用CRUD 使用通用存储库模式

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.