Простой совокупный корень и репозиторий

Я один из многих, кто пытается понять концепцию совокупных корней, и я думаю, что у меня это есть! Однако, когда я начал моделировать этот примерный проект, я быстро столкнулся с дилеммой.

У меня есть две сущности ProcessType и Process. Process не может существовать без ProcessType, а ProcessType имеет много Process. Таким образом, процесс содержит ссылку на тип и не может существовать без него.

Так должен ли ProcessType быть совокупным корнем? Новые процессы будут созданы путем вызова processType.AddProcess(new Process()); Однако у меня есть другие сущности, которые содержат только ссылку на Process и получают доступ к его типу через Process.Type. В этом случае нет смысла сначала проходить ProcessType.

Но сущностям AFAIK за пределами агрегата разрешено хранить ссылки только на корень агрегата, а не на сущности внутри агрегата. Итак, у меня есть два агрегата, каждый со своим собственным репозиторием?


person Vern    schedule 07.02.2011    source источник


Ответы (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>();
person David Masters    schedule 08.02.2011
comment
Очень интересный подход Дэвида. Спасибо, что поделились этим! Как вы реализовали LookupService? В качестве службы инфраструктуры с универсальным методом Get, который напрямую использует базовую структуру ORM без каких-либо репозиториев? Может ли кто-нибудь еще прокомментировать этот подход? Вы делали что-то подобное или решили эту проблему по-другому? - person Vern; 08.02.2011
comment
У меня есть ILookupService, определенный в моей инфраструктуре, а затем реализация в отдельном проекте, который загружается с использованием DI (единство). Реализация - это необработанный SQL / Sprocs, но, конечно, вы можете легко использовать ORM. Эта служба может использоваться доменом и службами запросов для предоставления ViewModels пользовательскому интерфейсу. Причина, по которой я использую универсальные шаблоны, заключается в том, что все «LookupItems» просто имеют идентификатор, описание и список атрибутов. Это означает, что я могу использовать общие средства хранения данных для всех поисковых запросов / элементов поиска, используя их имя типа. I.E. У меня не было бы таблицы под названием «Тип процесса». - person David Masters; 08.02.2011
comment
Понятно. Я просто попытался реализовать это, и это кажется элегантным решением. Пока я буду придерживаться этого подхода, если кто-то не придет и не скажет, что у него есть лучшее решение :-) Я оставлю вопрос открытым в течение нескольких дней, чтобы получить больше мнений по этой теме. - person Vern; 08.02.2011
comment
Я согласен и уважительно не согласен. Я тоже использую службу поиска, чтобы инкапсулировать многие простые элементы, такие как заголовок, которые необходимы для приложения, но несущественны с точки зрения концепции. Объекты уровня знаний OTOH часто являются объектами-ценностями, но являются важной особенностью модели, поскольку они являются основными определениями. Если ProcessType является важной особенностью вашей модели, а я думаю, что это, вероятно, так, предоставьте ей собственный репозиторий / службу. Не прячьте важный определяющий аспект вашей модели в общем поиске исключительно из-за неправильного представления о том, что у объектов-значений нет репозиториев. - person Sisyphus; 09.02.2011
comment
@Sisyphus, действительно, мое решение имело бы смысл только в том случае, если 'ProcessType' представляет собой простой объект значения 'маркера', который просто имеет идентификатор, описание и некоторые атрибуты (например, process.ProcessType.IsSomething), и, прочитав его вопрос, это все, что кажется быть. Я не предлагаю, чтобы все объекты значений использовали общую службу поиска, только эти типы объектов, которые просто используются для группировки агрегатов с точки зрения запроса и, возможно, предоставляют конфигурацию с помощью своих атрибутов. - person David Masters; 09.02.2011
comment
@Vern - у тебя получилось? Я заметил, что вы задали 3 вопроса, но не приняли ни одного ответа: P - person David Masters; 16.02.2011

Не все так просто. ProcessType - это, скорее всего, объект уровня знаний - он определяет определенный процесс. С другой стороны, процесс - это экземпляр процесса, который называется ProcessType. Вероятно, вам действительно не нужны или не нужны двунаправленные отношения. Вероятно, процесс не является логическим потомком ProcessType. Обычно они принадлежат чему-то еще, например продукту, фабрике или последовательности.

Также по определению, когда вы удаляете совокупный корень, вы удаляете все члены совокупности. Когда вы удаляете процесс, я серьезно сомневаюсь, что вы действительно хотите удалить ProcessType. Если вы удалили ProcessType, вы, возможно, захотите удалить все процессы этого типа, но эта связь уже не идеальна, и есть вероятность, что вы не будете удалять объекты определения, как только у вас будет исторический процесс, который определяется ProcessType.

Я бы удалил коллекцию процессов из ProcessType и нашел более подходящего родителя, если он существует. Я бы оставил ProcessType членом Process, поскольку он, вероятно, определяет процесс. Объекты рабочего уровня (Process) и уровня знаний (ProcessType) редко работают как единый агрегат, поэтому я бы либо Process был агрегированным корнем, либо, возможно, нашел агрегированный корень, который является родительским для процесса. Тогда ProcessType будет внешним классом. Process.Type, скорее всего, избыточен, поскольку у вас уже есть Process.ProcessType. Просто избавься от этого.

У меня похожая модель в сфере здравоохранения. Есть процедура (операционный уровень) и ProcedureType (уровень знаний). ProcedureType - это отдельный класс. Процедура является потомком третьего объекта Encounter. Встреча - это совокупный корень для процедуры. У процедуры есть ссылка на ProcedureType, но это односторонний. Тип процедуры - это объект определения, он не содержит коллекции процедур.

ИЗМЕНИТЬ (потому что комментарии настолько ограничены)

Одна вещь, о которой нужно помнить при всем этом. Многие из них являются пуристами DDD и непреклонны в отношении правил. Однако, если вы внимательно прочитаете Эванса, он постоянно поднимает вероятность того, что часто требуются компромиссы. Он также очень подробно описывает логические и тщательно продуманные дизайнерские решения в сравнении с такими вещами, как команды, которые не понимают целей или обходят такие вещи, как агрегаты, ради удобства.

Важно понимать и применять концепции, а не правила. Я вижу много DDD, которые запихивают приложение в нелогичные и запутанные агрегаты и т. Д. Только потому, что применяется буквальное правило о репозиториях или обходе. брать.

Итак, каковы здесь ключевые концепции:

Агрегаты позволяют сделать сложную систему более управляемой, сводя поведение многих объектов к поведению ключевых игроков более высокого уровня.

Агрегаты предоставляют средства, гарантирующие, что объекты создаются в логическом и всегда допустимом состоянии, которое также сохраняет логическую единицу работы при обновлениях и удалениях.

Рассмотрим последний пункт. Во многих обычных приложениях кто-то создает набор объектов, которые не заполнены полностью, потому что им нужно только обновить или использовать несколько свойств. Приходит следующий разработчик, и ему тоже нужны эти объекты, и кто-то уже сделал где-то по соседству набор для другого назначения. Теперь этот разработчик решает просто использовать их, но затем обнаруживает, что у них нет всех необходимых ему свойств. Поэтому он добавляет еще один запрос и заполняет еще несколько свойств. В конце концов, потому что команда не придерживается ООП, потому что они придерживаются общей позиции, что ООП «неэффективно и непрактично для реального мира и вызывает проблемы с производительностью, такие как создание полных объектов для обновления одного свойства». В итоге они получают приложение, полное встроенного кода SQL и объектов, которые по существу случайным образом материализуются где угодно. Хуже того, эти объекты являются недействительными прокси-серверами. Процесс кажется процессом, но это не так, он частично заполняется различными способами в любой заданной точке в зависимости от того, что было необходимо. В итоге вы получаете множество запросов для постоянного частичного заполнения объектов в различной степени и часто много посторонней ерунды, например, нулевых проверок, которые не должны существовать, но требуются, потому что объект никогда не является действительным и т. Д.

Правила агрегирования предотвращают это, гарантируя, что объекты создаются только в определенных логических точках и всегда с полным набором допустимых отношений и условий. Итак, теперь, когда мы полностью понимаем, для чего нужны агрегированные правила и от чего они нас защищают, мы также хотим понять, что мы также не хотим злоупотреблять этими правилами и создавать странные агрегаты, которые не отражают то, что на самом деле представляет собой наше приложение, просто потому, что эти сводные правила существуют и должны соблюдаться всегда.

Поэтому, когда Эванс говорит создавать репозитории только для агрегатов, он говорит, что нужно создавать агрегаты в допустимом состоянии и сохранять их таким образом, вместо того, чтобы напрямую обходить агрегат для внутренних объектов. У вас есть процесс в качестве корневого агрегата, поэтому вы создаете репозиторий. ProcessType не является частью этого агрегата. Что вы делаете? Хорошо, если объект сам по себе и это сущность, это совокупность 1. Вы создаете для него репозиторий.

Теперь пуристы придут и скажут, что у вас не должно быть этого репозитория, потому что ProcessType - это объект значения, а не сущность. Следовательно, ProcessType вообще не является агрегатом, и поэтому вы не создаете для него репозиторий. Ну так что ты делаешь? Чего вы не делаете, так это втыкайте ProcessType в какую-то искусственную модель только по той причине, что вам нужно ее получить, поэтому вам нужен репозиторий, но чтобы иметь репозиторий, у вас должна быть сущность в качестве совокупного корня. Что вы делаете, так это внимательно рассматриваете концепции. Если кто-то говорит вам, что репозиторий неправильный, но вы знаете, что он вам нужен, и что бы они ни говорили, ваша система репозитория действительна и сохраняет ключевые концепции, вы сохраняете репозиторий как есть вместо того, чтобы искажать свою модель для удовлетворения догм.

Теперь в этом случае, если предположить, что я прав в отношении того, что такое ProcessType, как заметил другой комментатор, на самом деле это объект значения. Вы говорите, что это не может быть объект-значение. Это могло быть по нескольким причинам. Возможно, вы так говорите, потому что, например, используете NHibernate, но модель NHibernate для реализации объектов значений в той же таблице, что и другой объект, не работает. Таким образом, для вашего ProcessType требуется столбец и поле идентификации. Часто из соображений базы данных единственной практической реализацией является наличие объектов значений с идентификаторами в их собственной таблице. Или, может быть, вы так говорите, потому что каждый процесс указывает на один тип процесса по ссылке.

Не важно. Это объект-значение из-за концепции. Если у вас есть 10 объектов Process, которые имеют один и тот же ProcessType, у вас есть 10 членов и значений Process.ProcessType. Независимо от того, указывает ли каждый Process.ProcessType на одну ссылку или каждый получил копию, по определению все они должны быть в точности одинаковыми и полностью взаимозаменяемыми с любыми другими 10. ЭТО то, что делает его объектом значения. Человек, который говорит: «У него есть идентификатор, следовательно, он не может быть ценным объектом, который у вас есть», делает догматическую ошибку. Не делайте ту же ошибку, если вам нужно поле идентификатора, укажите его, но не говорите «это не может быть объект значения», когда на самом деле это тот, который по другой причине вам пришлось указать идентификатор к.

Так как же понять, что правильно, а что нет? ProcessType - это объект значения, но по какой-то причине у него должен быть идентификатор. Идентификатор сам по себе не нарушает правил. Вы понимаете это правильно, имея 10 процессов, каждый из которых имеет один и тот же ProcessType. Может быть, у каждого есть локальная глубокая копия, может быть, все они указывают на один объект. но все они идентичны в любом случае, например, каждый имеет Id = 2. Вы ошибаетесь, когда делаете это: каждый из 10 процессов имеет свой тип процесса, и этот тип процесса идентичен и полностью взаимозаменяем, ЗА ИСКЛЮЧЕНИЕМ теперь каждый также имеет свой собственный уникальный идентификатор. Теперь у вас есть 10 экземпляров одного и того же объекта, но они различаются только идентификатором и всегда будут отличаться только идентификатором. Теперь у вас больше нет объекта-значения не потому, что вы дали ему идентификатор, а потому, что вы дали ему идентификатор с реализацией, которая отражает природу сущности - каждый экземпляр уникален и отличается

Есть смысл?

person Sisyphus    schedule 07.02.2011
comment
Большое спасибо - вы абсолютно правы. Но, по словам Эванса, в своей синей книге он говорит, что вы создаете репозитории для агрегатов. Однако в моем сценарии мне нужен репозиторий для автономного класса (ProcessType), потому что мне нужно иметь возможность создавать новые типы процессов. Не то чтобы это случалось очень часто, но это вариант. Как бы вы с этим справились? - person Vern; 07.02.2011
comment
Под созданием я подразумеваю сохранение объекта в базе данных, для чего я использую шаблон репозитория. Репозиторий не несет ответственности за создание объектов. - person Vern; 07.02.2011
comment
При использовании дизайна на основе уровня знаний подумайте, должно ли само создание знаний быть частью основного приложения. Зависит от. В моем случае - миллионы процедур и симптомов. Нужен не в системе? Для этого есть приложение. И один старший врач, который им пользуется. Итак, у нас есть новый пациент, у которого есть ягоды дингле поедания плоти, это 1 из 10 известных случаев. Человек наверху скажет, что для этого есть запись. Доктора мочатся и стонут, но, уступив им дорогу, через месяц они сделают свою собственную систему непригодной для использования. Другая проблема получает другое приложение, не являющееся частью основного. - person Sisyphus; 07.02.2011
comment
Это очень интересный момент. Думаю, что сейчас попробую эту стратегию. Но что, если новые типы добавляются чаще? Другой пример может быть, если у вас есть ряд продуктов, которые вы группируете вместе, имея сущность ProductGroup с одним ко многим для продуктов. Куда бы вы поместили ProductGroup? То же самое. Продукт должен быть в группе, но другие объекты могут содержать ссылку непосредственно на продукт. Однако в этом случае удаление и добавление новых групп является нормальным. Как бы вы с этим справились? - person Vern; 07.02.2011
comment
Просто прочтите редактирование. Ваше мнение о ВО имеет смысл, и, возможно, мой ProcessType - это ВО. Но давайте возьмем классический пример. В моем графическом интерфейсе, когда я создаю новый процесс, мне нужно выбрать ProcessType из раскрывающегося списка. Поэтому мне нужен метод в репозитории для извлечения всех типов процессов из моей базы данных ProcessTypeRepository.GetAllProcessTypes(). Если тип не подходит для моего процесса, я смогу создать новый ProcessType, ProcessTypeRepository.AddProcessType(ProcessType pt). Все эти методы, на мой взгляд, указывают на то, что это не голосовое голосование. Но может я ошибаюсь? Продолжение в следующем комментарии. - person Vern; 07.02.2011
comment
Такое же обсуждение ведется на здесь, в комментариях. См. Комментарий Аарона от 25.04.2007 в 17:11. К сожалению, автор на него не отвечает. - person Vern; 07.02.2011
comment
Этот блог вводит в заблуждение и ведет вас по пути, где все, что имеет имя, является сущностью. Идентичность с точки зрения ценности по сравнению с сущностью - это идентичность экземпляра. Если для процесса вам нужен конкретный тип ProcessType, и в будущем могут появиться новые, это все равно объект значения. Если ProcessType - это сущность, которая подразумевает, что при получении Process вы также должны получить конкретный экземпляр ProcessType, созданный для этого процесса, и что ваша эквивалентность Process.Type и ProcessType недействительна. Перечитайте по этому поводу Evans pg 150. Обычно он говорит, и часто я даже этим прошу не согласиться. - person Sisyphus; 09.02.2011

Слушай, я думаю, тебе нужно реструктурировать свою модель. Используйте ProcessType как объект значения и обработайте Agg Root. Таким образом, у каждого процесса есть тип процесса

Public class Process
{
      Public Process()
      {

      }

      public ProcessType { get; }

}

для этого вам просто нужен 1 агг корень, а не 2.

person Pedro de la Cruz    schedule 07.02.2011
comment
В этом сценарии ProcessType не может быть объектом значения. В модели нет смысла. Так что, боюсь, это не выход. - person Vern; 07.02.2011
comment
:) проверьте ответы выше: P - person Pedro de la Cruz; 18.02.2011