Простой совокупный корень и хранилище
Я один из многих, кто пытается понять концепцию совокупных корней, и я думаю, что я понял! Однако, когда я начал моделировать этот пример проекта, я быстро столкнулся с дилеммой.
У меня есть две сущности ProcessType
а также Process
, Process
не может существовать без ProcessType
и ProcessType
имеет много Process
эс. Таким образом, процесс содержит ссылку на тип и не может существовать без него.
Так должно ProcessType
быть совокупным корнем? Новые процессы будут созданы путем вызова processType.AddProcess(new Process())
; Тем не менее, у меня есть другие объекты, которые содержат только ссылку на Process
и получает доступ к его типу через Process.Type
, В этом случае нет смысла проходить ProcessType
первый.
Но сущности AFAIK вне агрегата могут хранить только ссылки на корень агрегата, а не сущности внутри агрегата. Итак, у меня есть два агрегата, каждый со своим репозиторием?
3 ответа
Я в основном согласен с тем, что сказал Сизиф, особенно с тем, что нельзя ограничивать себя "правилами" DDD, которые могут привести к довольно нелогичному решению.
С точки зрения вашей проблемы, я сталкивался с ситуацией много раз, и я бы назвал "ProcessType" поиском. Поиски - это объекты, которые "определяют" и не имеют ссылок на другие объекты; в терминологии DDD они являются ценностными объектами. Другими примерами того, что я бы назвал поиском, может быть RoleType члена команды, который может быть, например, тестером, разработчиком, менеджером проекта. Даже "звание" человека я бы определил как поиск - мистер, мисс, миссис, доктор
Я бы смоделировал ваш агрегат процесса как:
public class Process
{
public ProcessType { get; }
}
Как вы говорите, объекты такого типа обычно должны заполнять выпадающие списки в пользовательском интерфейсе и, следовательно, должны иметь собственный механизм доступа к данным. Тем не менее, я лично НЕ создал "репозитории" как таковые для них, а скорее "LookupService". Для меня это сохраняет элегантность DDD, сохраняя "хранилища" строго для совокупных корней.
Вот пример обработчика команд на моем сервере приложений и как я реализовал это:
Агрегат члена команды:
public class TeamMember : Person
{
public Guid TeamMemberID
{
get { return _teamMemberID; }
}
public TeamMemberRoleType RoleType
{
get { return _roleType; }
}
public IEnumerable<AvailabilityPeriod> Availability
{
get { return _availability.AsReadOnly(); }
}
}
Обработчик команд:
public void CreateTeamMember(CreateTeamMemberCommand command)
{
TeamMemberRoleType role = _lookupService.GetLookupItem<TeamMemberRoleType>(command.RoleTypeID);
TeamMember member = TeamMemberFactory.CreateTeamMember(command.TeamMemberID,
role,
command.DateOfBirth,
command.FirstName,
command.Surname);
using (IUnitOfWork unitOfWork = UnitOfWorkFactory.CreateUnitOfWork())
_teamMemberRepository.Save(member);
}
Клиент также может использовать LookupService для заполнения раскрывающихся списков и т.д.:
ILookup<TeamMemberRoleType> roles = _lookupService.GetLookup<TeamMemberRoleType>();
Не все так просто. ProcessType больше всего похож на объект уровня знаний - он определяет определенный процесс. Процесс, с другой стороны, является экземпляром процесса, который является ProcessType. Вы, вероятно, действительно не нуждаетесь или не хотите двунаправленных отношений. Процесс, вероятно, не является логическим потомком ProcessType. Обычно они принадлежат чему-то другому, например, продукту, фабрике или последовательности.
Также по определению при удалении корня агрегата удаляются все элементы агрегата. Когда вы удаляете Process, я серьезно сомневаюсь, что вы действительно хотите удалить ProcessType. Если вы удалили ProcessType, возможно, вы захотите удалить все процессы этого типа, но эта связь уже не идеальна, и есть вероятность, что вы не будете удалять объекты определений, как только у вас будет исторический процесс, определенный ProcessType.
Я бы удалил коллекцию Processes из ProcessType и нашел бы более подходящего родителя, если таковой существует. Я бы оставил ProcessType в качестве члена Process, поскольку он, вероятно, определяет Process. Объекты операционного уровня (Process) и уровня знаний (ProcessType) редко работают как один агрегат, поэтому я бы выбрал либо Process быть агрегатным корнем, либо, возможно, нашел бы агрегатный корень, который является родительским для процесса. Тогда ProcessType будет внешним классом. Process.Type, скорее всего, избыточен, так как у вас уже есть Process.ProcessType. Просто избавься от этого.
У меня похожая модель для здравоохранения. Существует Процедура (Операционный уровень) и ПроцедураТип (уровень знаний). Процедура Type является отдельным классом. Процедура является потомком третьего объекта Encounter. Encounter является совокупным корнем для процедуры. Процедура имеет ссылку на тип процедуры, но это один из способов. Процедура Type - это объект определения, который не содержит коллекцию процедур.
РЕДАКТИРОВАТЬ (потому что комментарии настолько ограничены)
Во всем этом нужно помнить одну вещь. Многие из них являются пуристами DDD и непреклонны в отношении правил. Однако, если вы внимательно прочитаете Эванса, он постоянно повышает вероятность того, что компромиссы часто требуются. Он также идет на многое, чтобы охарактеризовать логические и тщательно продуманные дизайнерские решения по сравнению с такими вещами, как команды, которые не понимают цели или обходят такие вещи, как агрегаты, для удобства.
Важно понимать и применять концепции в отличие от правил. Я вижу много DDD, которые вводят приложение в нелогичные и сбивающие с толку агрегаты и т.д. только по той причине, что применяется буквальное правило о репозиториях или обходе. Это не является целью DDD, но часто является результатом чрезмерно догматического подхода многих брать.
Итак, каковы основные понятия здесь:
Агрегаты предоставляют средства для того, чтобы сделать сложную систему более управляемой, уменьшив поведение многих объектов до более высокого уровня поведения ключевых игроков.
Агрегаты предоставляют средства для обеспечения того, чтобы объекты создавались в логическом и всегда действительном состоянии, которое также сохраняет логическую единицу работы при обновлении и удалении.
Давайте рассмотрим последний пункт. Во многих обычных приложениях кто-то создает набор объектов, которые не полностью заполнены, потому что им нужно только обновить или использовать несколько свойств. Приходит следующий разработчик, ему тоже нужны эти объекты, и кто-то уже сделал набор где-то по соседству для другой цели. Теперь этот разработчик решает просто использовать их, но затем он обнаруживает, что они не обладают всеми необходимыми им свойствами. Поэтому он добавляет еще один запрос и заполняет еще несколько свойств. В конце концов, потому что команда не придерживается ООП, потому что они придерживаются общего мнения, что ООП "неэффективно и нецелесообразно для реального мира и вызывает проблемы с производительностью, такие как создание полных объектов для обновления одного свойства". В итоге они получают приложение, полное встроенного кода SQL и объектов, которые по существу случайным образом материализуются где угодно. Хуже того, эти объекты являются недействительными проклятыми прокси. Процесс представляется процессом, но это не так, он частично заполняется различными способами в зависимости от того, что было необходимо. В результате вы получаете множество запросов для непрерывного частичного заполнения объектов в разной степени и часто множества посторонних дерьмов, таких как нулевые проверки, которые не должны существовать, но требуются, потому что объект никогда не является действительно действительным и т. Д.
Совокупные правила предотвращают это, гарантируя, что объекты создаются только в определенных логических точках и всегда с полным набором действительных отношений и условий. Итак, теперь, когда мы полностью понимаем, для чего именно нужны агрегатные правила и от чего они нас защищают, мы также хотим понять, что мы также не хотим злоупотреблять этими правилами и создаем странные агрегаты, которые не отражают то, чем на самом деле является наше приложение, просто потому, что эти совокупные правила существуют и должны соблюдаться всегда.
Поэтому, когда Эванс говорит о создании репозиториев только для агрегатов, он говорит, что создайте агрегаты в допустимом состоянии и сохраняйте их таким образом, вместо того чтобы напрямую обходить агрегат для внутренних объектов. У вас есть процесс в качестве корневого агрегата, поэтому вы создаете хранилище. ProcessType не является частью этого агрегата. Чем ты занимаешься? Хорошо, если объект сам по себе и является сущностью, это совокупность 1. Вы создаете хранилище для него.
Теперь придет пурист и скажет, что у вас не должно быть этого хранилища, потому что ProcessType является объектом значения, а не сущностью. Поэтому ProcessType вообще не является агрегатом, и поэтому вы не создаете для него хранилище. Ну так что ты делаешь? То, что вы не делаете, - это добавляете ShootHourn ProcessType в какую-то искусственную модель только по той причине, что вам нужно получить ее, поэтому вам нужен репозиторий, но чтобы иметь репозиторий, вы должны иметь сущность в качестве совокупного корня. Что вы делаете, это тщательно продумайте концепции. Если кто-то говорит вам, что хранилище неверно, но вы знаете, что оно вам нужно, и что бы они ни говорили, ваша система хранилища действительна и сохраняет ключевые концепции, вы сохраняете хранилище как есть, а не деформируете свою модель, чтобы удовлетворить догму.
Теперь в этом случае, если я правильно понял, что такое ProcessType, как заметил другой комментатор, это на самом деле объект значения. Вы говорите, что это не может быть Объектом Значения. Это может быть по нескольким причинам. Возможно, вы говорите так, потому что вы используете, например, NHibernate, но модель NHibernate для реализации объектов-значений в той же таблице, что и другой объект, не работает. Таким образом, ваш ProcessType требует столбца и поля идентификатора Часто из соображений базы данных единственной практической реализацией является создание объектов значений с идентификаторами в их собственной таблице. Или, может быть, вы говорите это, потому что каждый процесс указывает на один ProcessType по ссылке.
Это не имеет значения. Это значение объекта из-за концепции. Если у вас есть 10 объектов Process с одинаковым ProcessType, у вас есть 10 членов и значений Process.ProcessType. Независимо от того, указывает ли каждый Process.ProcessType на одну ссылку или каждая получает копию, они все равно по определению должны быть абсолютно одинаковыми и полностью взаимозаменяемыми с любым другим 10. Это то, что делает его значением Object. Человек, который говорит: "У него есть Id, следовательно, не может быть значением. У вас есть сущность" делает догматическую ошибку. Не делайте ту же ошибку, если вам нужно поле идентификатора, дайте его, но не говорите, что "это не может быть объект-значение", хотя на самом деле это хотя и то, что по другой причине вам пришлось дать Id к.
Так как вы понимаете это правильно и неправильно? ProcessType - это объект-значение, но по какой-то причине он должен иметь идентификатор. Идентификатор per se не нарушает правила. Вы понимаете это правильно, имея 10 процессов, которые имеют одинаковый ProcessType. Может быть, у каждого есть локальная глубокая копия, может быть, все они указывают на один объект. но каждый из них в любом случае идентичен, например, каждый имеет Id = 2. Когда вы делаете это, вы ошибаетесь: каждый из 10 процессов имеет ProcessType, и этот ProcessType идентичен и полностью взаимозаменяем, за исключением того, что каждый из них также имеет свой собственный уникальный идентификатор. Теперь у вас есть 10 экземпляров одного и того же, но они различаются только по Id и всегда будут отличаться только по Id. Теперь у вас больше нет объекта Value, не потому, что вы дали ему Id, а потому, что вы дали ему Id с реализацией, отражающей природу сущности - каждый экземпляр уникален и отличается
Есть смысл?
Слушай, я думаю, ты должен реструктурировать свою модель. Используйте ProcessType как объект Value и Process Agg Root. Таким образом, каждый процесс имеет тип процесса
Public class Process
{
Public Process()
{
}
public ProcessType { get; }
}
для этого вам просто нужно 1 агг корень не 2.