DDD, Entity Framework, Агрегированное поведение сущностей ( Person.AddEmail и т. Д.)
Вот простой пример проблемы, с которой я сталкиваюсь, которая не связана с некоторыми из представленных здесь идей и других мест, касающихся DDD.
Скажем, у меня есть сайт ASP.NET MVC 3, который создает / манипулирует человеком. Контроллеры получают доступ к уровню обслуживания приложений (PersonService), который, в свою очередь, использует доменные объекты (EF 4 POCO) и PersonRepository, чтобы вносить изменения и сохранять их. Я упускаю все интерфейсы здесь для простоты. В этом случае человек является корнем и для простоты имеет только адреса электронной почты (также предположим, что электронная почта не является неизменной и может быть обновлена).
Вариант 1: Попытайтесь придерживаться [моего понимания] основ 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: разрешите службе персоны использовать репозиторий для добавления / обновления адреса электронной почты и не реализовывать поведение для сущности персоны. Это намного проще в случае многих-многих отношений (например, адреса, где для завершения работы необходимо обновить две таблицы), но затем модель становится "анемичной", являясь просто набором методов получения и установки.
// 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; }
}
В любом случае, есть мысли по этому поводу?
Спасибо Брайан
2 ответа
Вариант 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.
Кстати, ваш EmailAddress здесь не сущность, не идентификатор (просто опечатка, я думаю). А как вы связываете свой адрес электронной почты с человеком?