领域驱动设计-DDD、实体框架、聚合实体行为(Person.AddEmail等)



这里有一个我遇到的问题的简单例子,它与这里和其他地方提出的关于DDD的一些想法不一致。

假设我有一个ASP.NET MVC 3网站,它可以创建/操纵一个人。控制器访问一个应用程序服务层(PersonService),该层又使用域实体(EF 4 POCO)和PersonRepository进行更改并将其持久化。为了简单起见,我省略了所有接口。在这种情况下,Person是根,为了简单起见,它只有电子邮件地址(也假设电子邮件不是不可变的,并且可以更新)。

选项1:试着坚持[我的理解]DDD的基本知识,在DDD中,与实体直接相关的行为是作为实体的一部分实现的(Person实现AddEmail、ChangeEmail等)。除了Add*方法之外,唯一的问题是,Person需要了解上下文或实体框架部分(这将消除任何持久性的无知),或者需要使用"服务"或存储库将电子邮件标记为已修改。

// Person Service
public class PersonService {
    // constructor injection to get unit of work and person repository...
    // ...methods to add/update a person
    public EmailAddress AddEmailAddress(int personId, EmailAddress email)
    {   
        Person p = personRepository.Find(p => p.Id == personId).First();
        p.AddEmail(email);   
        uow.SaveChanges();
        return email; 
    }
    public EmailAddress ChangeEmailAddress(EmailAddress email)
    {
        Person p = personRepository.Find(p => p.Id == personId).First();
        p.ChangeEmail(email);   
        // change state of email object here so it's updated in the next line???
        // if not here, wouldn't the Person entity have to know about the context
        // or use a service?
        uow.SaveChanges();
        return email;    
    }
}
// Person Repository
public class PersonRepository
{
   // generic repository implementation
}
// Person Entity
public class Person
{
    public string Name { get;set; }
    public IEnumerable<EmailAddress> EmailAddresses { get;set; }
    public void AddEmail(EmailAddress email)
    {
        this.EmailAddresses.Add(email);
    }
    public void ChangeEmail(EmailAddress email)
    {
        EmailAddress orig = this.EmailAddresses.First(e => e.Id == email.id);
        // update properties on orig
        // NOW WHAT? [this] knows nothing about the context in order to change state,
        etc, or do anything to mark the email add updated
    }
}
// Email 
public class EmailAddress
{
    public string Email { get;set; }
    public bool IsPrimary { get;set; }
}

选项2:让个人服务使用存储库来添加/更新电子邮件地址,而不在个人实体上实现该行为。在多对多关系的情况下(例如,address,其中需要更新两个表来完成工作),这要简单得多,但模型随后变得"贫血",只是一群getter和setter。

// Person Service
public class PersonService {
    // constructor injection to get unit of work and person repository...
    // ...methods to add/update a person
    public EmailAddress AddEmailAddress(int personId, EmailAddress email)
    {   
        Person p = personRepository.Find(p => p.Id == personId).First();
        personRepository.AddEmail(personId, email);   
        uow.SaveChanges();
        return email; 
    }
    public EmailAddress ChangeEmailAddress(EmailAddress email)
    {
        personRepository.ChangeEmail(email);   
        uow.SaveChanges();
        return email;    
    }
}
// Person Repository
public class PersonRepository
{
   // generic repository implementation
}
// Person Entity
public class Person
{
    public string Name { get;set; }
    public IEnumerable<EmailAddress> EmailAddresses { get;set; }
}
// Email 
public class EmailAddress
{
    public string Email { get;set; }
    public bool IsPrimary { get;set; }
}

不管怎样,你对此有什么想法吗?

谢谢,Brian

选项1就是我们要走的路。

推理很简单——更改电子邮件地址是领域问题。我敢打赌,你的领域专家已经说过,他们将需要更改电子邮件。这会自动将电子邮件更改逻辑标记为业务逻辑,该逻辑应该存在于域模型中。对象主要由它们的行为定义,而不是由它们所持有的数据定义。

此外,在您选择使用工作单元模式并包装服务中的所有内容之前,请三思而后行。聚合根应该绘制事务边界,如果服务只是包装存储库和域对象调用,那么它们通常是无用的。

我想要这样的东西:

public class Person{
  public Email Email{get;private set;}
  public void SpecifyEmail(Email email){
    //some validation, if necessary
    EnsureEmailCanBeChanged();
    //applying state changes
    Email=email;
    //raising event, if necessary
    Raise(new EmailChanged(this));
  }
  public class EmailChanged:Event<Person>{
    public EmailChanged(Person p):base(p){}
  }
}
public class Email{
  public Email(string email){
    //validations (e.g. email format)
    Value=email;
  }
  //implicit to string, explicit from string conversions
}
public class PersonController{
  public ActionResult SpecifyEmail(int person, string email){
    _persons.Get(person).SpecifyEmail((Email)email);
    return RedirectToAction("Person",new{person});
  }
}

我正在使用NHibernate——它足够聪明,可以弄清楚自从上次Person被持久化以来发生了什么变化。很难说实体框架是如何处理这一问题的。

我是NH用户,可能不知道所有EF限制,但一般来说,无论ORM的限制是什么,实体都应该尽可能干净。服务层已经与数据访问相耦合,因此不会造成任何危害。

我相信EF4应该知道如何跟踪收集的变化。如果没有,那么最好的方法是将添加/删除逻辑留在Person实体中,并在PersonService中持久存在。

顺便说一句,你的电子邮件地址在这里不是一个实体,没有Id(我想只是打字错误)。如何将您的电子邮件地址链接到个人?

最新更新