HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-03-10
1 <p>В этой статье хотелось бы рассказать о таком понятии, как агрегат в Domain- Driven-Design (DDD), а именно о его преимуществах в контексте транзакционности изменений и группировки бизнес-логики. Пожалуй, из всех так называемых тактических шаблонов в DDD этот часто является самым важным и трудным для понимания. Об агрегатах имеет смысл поговорить, упомянув также шаблон репозиторий.</p>
1 <p>В этой статье хотелось бы рассказать о таком понятии, как агрегат в Domain- Driven-Design (DDD), а именно о его преимуществах в контексте транзакционности изменений и группировки бизнес-логики. Пожалуй, из всех так называемых тактических шаблонов в DDD этот часто является самым важным и трудным для понимания. Об агрегатах имеет смысл поговорить, упомянув также шаблон репозиторий.</p>
2 <h3>Репозиторий и агрегат</h3>
2 <h3>Репозиторий и агрегат</h3>
3 <p><strong>Репозиторий</strong>- это очень популярный шаблон для реализации слоя доступа к данным в .NET-приложениях, даже если они не используют шаблоны DDD. Однако, изначально он рассматривался в контексте работы с объектной моделью предметной области именно в контексте DDD, для того чтобы получить из базы данных агрегат и работать с коллекцией агрегатов.</p>
3 <p><strong>Репозиторий</strong>- это очень популярный шаблон для реализации слоя доступа к данным в .NET-приложениях, даже если они не используют шаблоны DDD. Однако, изначально он рассматривался в контексте работы с объектной моделью предметной области именно в контексте DDD, для того чтобы получить из базы данных агрегат и работать с коллекцией агрегатов.</p>
4 <p><strong>Агрегат</strong>- набор объектов предметной области, которые сохраняются в источнике данных (например, реляционной БД), используются совместно в логике приложения, часто имеют связи на уровне базы данных, а также имеют свойство меняться в рамках одной транзакции по бизнес-процессу; агрегат представлен корневой сущностью с ссылками на зависимые объекты. К зависимым объектам мы получаем доступ именно через корень агрегата. Вся бизнес-логика для работы с данными внутри агрегата происходит через корень.</p>
4 <p><strong>Агрегат</strong>- набор объектов предметной области, которые сохраняются в источнике данных (например, реляционной БД), используются совместно в логике приложения, часто имеют связи на уровне базы данных, а также имеют свойство меняться в рамках одной транзакции по бизнес-процессу; агрегат представлен корневой сущностью с ссылками на зависимые объекты. К зависимым объектам мы получаем доступ именно через корень агрегата. Вся бизнес-логика для работы с данными внутри агрегата происходит через корень.</p>
5 <p>Некоторые NoSQL базы можно вполне считать "агрегатоориентированными", например, документы в Mongo представляют из себя именно агрегатный формат хранения данных, транзакционность гарантируется на уровне одного документа. Более детально эта идея раскрыта в книге "NoSQL Distilled" Мартина Фаулера.</p>
5 <p>Некоторые NoSQL базы можно вполне считать "агрегатоориентированными", например, документы в Mongo представляют из себя именно агрегатный формат хранения данных, транзакционность гарантируется на уровне одного документа. Более детально эта идея раскрыта в книге "NoSQL Distilled" Мартина Фаулера.</p>
6 <p>Теперь стоит рассмотреть в чем преимущества использования агрегатов при реализации бизнес-логики.</p>
6 <p>Теперь стоит рассмотреть в чем преимущества использования агрегатов при реализации бизнес-логики.</p>
7 <h3>Задача</h3>
7 <h3>Задача</h3>
8 <p>Давайте рассмотрим некоторую часть информационной системы для работы с клиентами, клиент имеет некоторый набор данных, несколько мест работы и контакты. Сценарий использования данных выглядит примерно так: мы находим нужного клиента по ФИО или создаем нового, переходим в его карточку, можем добавлять, удалять контакты, места работы и редактировать общие данные, при нажатии на кнопку "Сохранить" происходит сохранение всех данных, при добавлении и удалении контактов или мест работы происходит автоматическое сохранение всех данных клиента, если клиент новый, то все данные сохраняются только по кнопке.</p>
8 <p>Давайте рассмотрим некоторую часть информационной системы для работы с клиентами, клиент имеет некоторый набор данных, несколько мест работы и контакты. Сценарий использования данных выглядит примерно так: мы находим нужного клиента по ФИО или создаем нового, переходим в его карточку, можем добавлять, удалять контакты, места работы и редактировать общие данные, при нажатии на кнопку "Сохранить" происходит сохранение всех данных, при добавлении и удалении контактов или мест работы происходит автоматическое сохранение всех данных клиента, если клиент новый, то все данные сохраняются только по кнопке.</p>
9 <p>Нужно описать эту структуру данных и реализовать CRUD. Рассмотрим только вариант создания нового клиента.</p>
9 <p>Нужно описать эту структуру данных и реализовать CRUD. Рассмотрим только вариант создания нового клиента.</p>
10 <p>Сначала можно реализовать три таблицы Customers, Contacts и JobPlaces. Код классов для работы с этими таблицами будут выглядеть так:</p>
10 <p>Сначала можно реализовать три таблицы Customers, Contacts и JobPlaces. Код классов для работы с этими таблицами будут выглядеть так:</p>
11 /// &lt;summary&gt; /// Клиент /// &lt;/summary&gt; public class Customer { /// &lt;summary&gt; /// Id /// &lt;/summary&gt; public Guid Id { get; set; } /// &lt;summary&gt; /// Полное имя клиента /// &lt;/summary&gt; public string FullName { get; set; } /// &lt;summary&gt; /// Канал привлечения (интернет-реклама, реклама на улице и т.д.) /// &lt;/summary&gt; public AcquisitionChannel Channel { get; set; } /// &lt;summary&gt; /// Дата создания /// &lt;/summary&gt; public DateTime CreatedDate { get; set; } /// &lt;summary&gt; /// Признак активности /// &lt;/summary&gt; public bool IsActive { get; set; } } /// &lt;summary&gt; /// Место работы /// &lt;/summary&gt; public class JobPlace { /// &lt;summary&gt; /// Id, уникальный идентификатор /// &lt;/summary&gt; public Guid Id { get; set; } /// &lt;summary&gt; /// Описание места работы /// &lt;/summary&gt; public string Description { get; set; } /// &lt;summary&gt; /// Дата начала работы /// &lt;/summary&gt; public DateTime StartDate { get; set; } /// &lt;summary&gt; /// Дата окончания работы /// &lt;/summary&gt; public DateTime? CompletionDate { get; set; } /// &lt;summary&gt; /// Идентификатор клиента /// &lt;/summary&gt; public Guid CustomerId { get; set; } } /// &lt;summary&gt; /// Контакт клиента /// &lt;/summary&gt; public class Contact { /// &lt;summary&gt; /// Id, уникальный идентификатор /// &lt;/summary&gt; public Guid Id { get; set; } /// &lt;summary&gt; /// Адрес электронной почты /// &lt;/summary&gt; public string Email { get; set; } /// &lt;summary&gt; /// Телефон /// &lt;/summary&gt; public string Phone { get; set; } /// &lt;summary&gt; /// Id клиента /// &lt;/summary&gt; public Guid CustomerId { get; set; } }<p>Для работы с базой данных создадим Generic-репозиторий с интерфейсом ниже, так как базового набора операций для каждой таблицы хватит:</p>
11 /// &lt;summary&gt; /// Клиент /// &lt;/summary&gt; public class Customer { /// &lt;summary&gt; /// Id /// &lt;/summary&gt; public Guid Id { get; set; } /// &lt;summary&gt; /// Полное имя клиента /// &lt;/summary&gt; public string FullName { get; set; } /// &lt;summary&gt; /// Канал привлечения (интернет-реклама, реклама на улице и т.д.) /// &lt;/summary&gt; public AcquisitionChannel Channel { get; set; } /// &lt;summary&gt; /// Дата создания /// &lt;/summary&gt; public DateTime CreatedDate { get; set; } /// &lt;summary&gt; /// Признак активности /// &lt;/summary&gt; public bool IsActive { get; set; } } /// &lt;summary&gt; /// Место работы /// &lt;/summary&gt; public class JobPlace { /// &lt;summary&gt; /// Id, уникальный идентификатор /// &lt;/summary&gt; public Guid Id { get; set; } /// &lt;summary&gt; /// Описание места работы /// &lt;/summary&gt; public string Description { get; set; } /// &lt;summary&gt; /// Дата начала работы /// &lt;/summary&gt; public DateTime StartDate { get; set; } /// &lt;summary&gt; /// Дата окончания работы /// &lt;/summary&gt; public DateTime? CompletionDate { get; set; } /// &lt;summary&gt; /// Идентификатор клиента /// &lt;/summary&gt; public Guid CustomerId { get; set; } } /// &lt;summary&gt; /// Контакт клиента /// &lt;/summary&gt; public class Contact { /// &lt;summary&gt; /// Id, уникальный идентификатор /// &lt;/summary&gt; public Guid Id { get; set; } /// &lt;summary&gt; /// Адрес электронной почты /// &lt;/summary&gt; public string Email { get; set; } /// &lt;summary&gt; /// Телефон /// &lt;/summary&gt; public string Phone { get; set; } /// &lt;summary&gt; /// Id клиента /// &lt;/summary&gt; public Guid CustomerId { get; set; } }<p>Для работы с базой данных создадим Generic-репозиторий с интерфейсом ниже, так как базового набора операций для каждой таблицы хватит:</p>
12 public interface IRepository&lt;T&gt; { Task&lt;IEnumerable&lt;T&gt;&gt; GetAllAsync(); Task&lt;T&gt; GetByIdAsync(Guid id); Task&lt;Guid&gt; AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(T entityId); }<p>В реализации будет код для работы с SQL-базой данных, это могут быть SQL-вызовы базы данных или вызовы хранимых процедур, функций или представлений.</p>
12 public interface IRepository&lt;T&gt; { Task&lt;IEnumerable&lt;T&gt;&gt; GetAllAsync(); Task&lt;T&gt; GetByIdAsync(Guid id); Task&lt;Guid&gt; AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(T entityId); }<p>В реализации будет код для работы с SQL-базой данных, это могут быть SQL-вызовы базы данных или вызовы хранимых процедур, функций или представлений.</p>
13 <p>Мы не рассматриваем вариант с ORM для более четкого понимания изначальной идеи репозиториев и агрегатов. Важно заметить, что этот интерфейс говорит нам о том, что вызывая операции Add, Update, Delete, мы предполагаем, что произойдет некоторое целостное изменение данных в коллекции объектов, которую представляет репозиторий и в базе данных также, то есть операции транзакционны.</p>
13 <p>Мы не рассматриваем вариант с ORM для более четкого понимания изначальной идеи репозиториев и агрегатов. Важно заметить, что этот интерфейс говорит нам о том, что вызывая операции Add, Update, Delete, мы предполагаем, что произойдет некоторое целостное изменение данных в коллекции объектов, которую представляет репозиторий и в базе данных также, то есть операции транзакционны.</p>
14 <p>Чтобы реализовать задачу нужно в методе сохранения клиента реализовать обновление или вставку в соответствующие таблицы, но по условию все данные сохраняются вместе, то есть обновление таблиц нужно производить в транзакции, но, как было сказано выше, методы репозиториев по умолчанию выполняют изменения в таблицах в отдельных транзакциях, так как внутри мы просто делаем Insert или Update-операцию на SQL, в этом случае нужно будет сделать механизм для управления транзакцией в методе сохранения, что уже неявным образом вносит зависимость от БД в код приложения.</p>
14 <p>Чтобы реализовать задачу нужно в методе сохранения клиента реализовать обновление или вставку в соответствующие таблицы, но по условию все данные сохраняются вместе, то есть обновление таблиц нужно производить в транзакции, но, как было сказано выше, методы репозиториев по умолчанию выполняют изменения в таблицах в отдельных транзакциях, так как внутри мы просто делаем Insert или Update-операцию на SQL, в этом случае нужно будет сделать механизм для управления транзакцией в методе сохранения, что уже неявным образом вносит зависимость от БД в код приложения.</p>
15 <p>Ниже добавляем реализацию через TransactionScope, можно также реализовать свою абстракцию вида IUnitOfWork, которая будет использовать TransactionScope.</p>
15 <p>Ниже добавляем реализацию через TransactionScope, можно также реализовать свою абстракцию вида IUnitOfWork, которая будет использовать TransactionScope.</p>
16 public async Task&lt;CustomerCreatedDto&gt; CreateCustomerAsync( CreateCustomerDto customerDto) { if (customerDto == null) throw new ArgumentNullException(nameof(customerDto)); using var transactionScope = new TransactionScope(); var customer = new Customer() { Id = Guid.NewGuid(), Channel = customerDto.Channel, CreatedDate = DateTime.Now, FullName = customerDto.FullName, IsActive = true, }; var customerId = await _customerRepository.AddAsync(customer); var jobPlaces = customerDto.JobPlaces?.Select(x =&gt; new JobPlace() { Id = Guid.NewGuid(), CustomerId = customerId, Description = x.Description, StartDate = x.StartDate, CompletionDate = x.CompletionDate }).ToList(); if (jobPlaces != null &amp;&amp; jobPlaces.Any()) { foreach (var jobPlace in jobPlaces) { await _jobPlaceRepository.AddAsync(jobPlace); } } var contacts = customerDto.Contacts?.Select(x =&gt; new Contact() { Id = Guid.NewGuid(), CustomerId = customerId, Email = x.Email, Phone = x.Phone }).ToList(); if (contacts != null &amp;&amp; contacts.Any()) { foreach (var contact in contacts) { await _contactRepository.AddAsync(contact); } } transactionScope.Complete(); return new CustomerCreatedDto() { Id = customer.Id }; }<p>Примерно такой код можно встретить во многих .NET-приложениях, он выглядит довольно громоздко и подвержен ошибкам в поддержке, например, использование инициализаторов, а не конструкторов ведет к тому, что логика создания объектов может быть разбросана по многим местам, это создает вероятность ошибок, явное использование транзакционности делает неочевидную связь со слоем доступа к данным, могут быть проблемы с юнит-тестами.</p>
16 public async Task&lt;CustomerCreatedDto&gt; CreateCustomerAsync( CreateCustomerDto customerDto) { if (customerDto == null) throw new ArgumentNullException(nameof(customerDto)); using var transactionScope = new TransactionScope(); var customer = new Customer() { Id = Guid.NewGuid(), Channel = customerDto.Channel, CreatedDate = DateTime.Now, FullName = customerDto.FullName, IsActive = true, }; var customerId = await _customerRepository.AddAsync(customer); var jobPlaces = customerDto.JobPlaces?.Select(x =&gt; new JobPlace() { Id = Guid.NewGuid(), CustomerId = customerId, Description = x.Description, StartDate = x.StartDate, CompletionDate = x.CompletionDate }).ToList(); if (jobPlaces != null &amp;&amp; jobPlaces.Any()) { foreach (var jobPlace in jobPlaces) { await _jobPlaceRepository.AddAsync(jobPlace); } } var contacts = customerDto.Contacts?.Select(x =&gt; new Contact() { Id = Guid.NewGuid(), CustomerId = customerId, Email = x.Email, Phone = x.Phone }).ToList(); if (contacts != null &amp;&amp; contacts.Any()) { foreach (var contact in contacts) { await _contactRepository.AddAsync(contact); } } transactionScope.Complete(); return new CustomerCreatedDto() { Id = customer.Id }; }<p>Примерно такой код можно встретить во многих .NET-приложениях, он выглядит довольно громоздко и подвержен ошибкам в поддержке, например, использование инициализаторов, а не конструкторов ведет к тому, что логика создания объектов может быть разбросана по многим местам, это создает вероятность ошибок, явное использование транзакционности делает неочевидную связь со слоем доступа к данным, могут быть проблемы с юнит-тестами.</p>
17 <p>На первый взгляд, это вполне логичный подход, как и использование репозитория на таблицу. Если мы хотим добавить новое место работы клиенту где-то еще, то мы просто сделаем вставку в таблицу JobPlaces через нужный репозиторий.</p>
17 <p>На первый взгляд, это вполне логичный подход, как и использование репозитория на таблицу. Если мы хотим добавить новое место работы клиенту где-то еще, то мы просто сделаем вставку в таблицу JobPlaces через нужный репозиторий.</p>
18 <p>Однако, при таком подходе по всей системе в классах, которые будут реализовывать бизнес-логику, можно увидеть очень большое количество зависимостей в виде репозиториев, - это первый сигнал, что классы перегружены ответственностью, это может быть десяток репозиториев на класс-сервис или контроллер.</p>
18 <p>Однако, при таком подходе по всей системе в классах, которые будут реализовывать бизнес-логику, можно увидеть очень большое количество зависимостей в виде репозиториев, - это первый сигнал, что классы перегружены ответственностью, это может быть десяток репозиториев на класс-сервис или контроллер.</p>
19 <p>Однако, если внимательно посмотреть на их использование для изменения данных, как в примере выше, то скорее всего мы увидим, что по сути код таких изменений можно было бы сгруппировать по-другому.</p>
19 <p>Однако, если внимательно посмотреть на их использование для изменения данных, как в примере выше, то скорее всего мы увидим, что по сути код таких изменений можно было бы сгруппировать по-другому.</p>
20 <p>В большинстве случаев нам не придется отдельно от формы клиента добавлять ему места работы или контакты, да и в принципе это действие скорее всего не имеет смысла в контексте предметной области, так как сама структура данных говорит нам, что они логически связаны.</p>
20 <p>В большинстве случаев нам не придется отдельно от формы клиента добавлять ему места работы или контакты, да и в принципе это действие скорее всего не имеет смысла в контексте предметной области, так как сама структура данных говорит нам, что они логически связаны.</p>
21 <p>Для того чтобы это реализовать, нужно рассматривать клиента, его контакты и места работы не как отдельные таблицы в БД, а как целостный агрегат, который должен изменяться вместе, - это позволит нам упростить код.</p>
21 <p>Для того чтобы это реализовать, нужно рассматривать клиента, его контакты и места работы не как отдельные таблицы в БД, а как целостный агрегат, который должен изменяться вместе, - это позволит нам упростить код.</p>
22 <p>Ниже код для агрегата клиента:</p>
22 <p>Ниже код для агрегата клиента:</p>
23 /// &lt;summary&gt; /// Клиент /// &lt;/summary&gt; public class Customer { public Guid Id { get; set; } public string FullName { get; set; } public AcquisitionChannel Channel { get; set; } public DateTime CreatedDate { get; set; } public bool IsActive { get; set; } public List&lt;JobPlace&gt; JobPlaces { get; set; } public List&lt;Contact&gt; Contacts { get; set; } }<p>На первый взгляд, почти ничего не изменилось, но у нас появились поля для доступа коллекциям связанных объектов.</p>
23 /// &lt;summary&gt; /// Клиент /// &lt;/summary&gt; public class Customer { public Guid Id { get; set; } public string FullName { get; set; } public AcquisitionChannel Channel { get; set; } public DateTime CreatedDate { get; set; } public bool IsActive { get; set; } public List&lt;JobPlace&gt; JobPlaces { get; set; } public List&lt;Contact&gt; Contacts { get; set; } }<p>На первый взгляд, почти ничего не изменилось, но у нас появились поля для доступа коллекциям связанных объектов.</p>
24 <p>Нужно обратить внимание, что это очень упрощенный пример модели клиента, ее можно назвать "анемичной", так как по сути она не содержит бизнес-логики, объектов- значений, как должно быть в правильном варианте реализации для DDD, именно такой формат объекта модели можно увидеть во многих .NET-приложениях, особенно, использующих ORM, так как ORM неявно подталкивает нас "агрегатному" формату описания модели данных, такая модель не позволит сразу воспользоваться всеми преимуществами "агрегатного" формата описания бизнес-логики, но позволит рассмотреть преимущества для реализации транзакционных изменений.</p>
24 <p>Нужно обратить внимание, что это очень упрощенный пример модели клиента, ее можно назвать "анемичной", так как по сути она не содержит бизнес-логики, объектов- значений, как должно быть в правильном варианте реализации для DDD, именно такой формат объекта модели можно увидеть во многих .NET-приложениях, особенно, использующих ORM, так как ORM неявно подталкивает нас "агрегатному" формату описания модели данных, такая модель не позволит сразу воспользоваться всеми преимуществами "агрегатного" формата описания бизнес-логики, но позволит рассмотреть преимущества для реализации транзакционных изменений.</p>
25 <h3>Реализация для агрегата в "анемичной" модели</h3>
25 <h3>Реализация для агрегата в "анемичной" модели</h3>
26 <p>Ниже типичный пример редактирования клиента в такой схеме.</p>
26 <p>Ниже типичный пример редактирования клиента в такой схеме.</p>
27 public async Task&lt;CustomerCreatedDto&gt; CreateCustomerAsync( CreateCustomerDto customerDto) { if (customerDto == null) throw new ArgumentNullException(nameof(customerDto)); var customer = new Customer() { Id = Guid.NewGuid(), Channel = customerDto.Channel, CreatedDate = DateTime.Now, FullName = customerDto.FullName, IsActive = true, JobPlaces = customerDto.JobPlaces?.Select(x =&gt; new JobPlace() { Id = Guid.NewGuid(), Description = x.Description, StartDate = x.StartDate, CompletionDate = x.CompletionDate }).ToList(), Contacts = customerDto.Contacts?.Select(x =&gt; new Contact() { Id = Guid.NewGuid(), Email = x.Email, Phone = x.Phone }).ToList() }; await _customerRepository.AddAsync(customer); return new CustomerCreatedDto() { Id = customer.Id }; }<p>Мы упростили код транзакционных изменений, теперь мы точно знаем, что сохранение произойдет в транзакции, во всяком случае мы ждем этого от репозитория. В итоге мы сосредоточились на создании объекта в правильном состоянии, в более сложном примере код был больше за счет различных проверок, получения дополнительных данных и т. д.</p>
27 public async Task&lt;CustomerCreatedDto&gt; CreateCustomerAsync( CreateCustomerDto customerDto) { if (customerDto == null) throw new ArgumentNullException(nameof(customerDto)); var customer = new Customer() { Id = Guid.NewGuid(), Channel = customerDto.Channel, CreatedDate = DateTime.Now, FullName = customerDto.FullName, IsActive = true, JobPlaces = customerDto.JobPlaces?.Select(x =&gt; new JobPlace() { Id = Guid.NewGuid(), Description = x.Description, StartDate = x.StartDate, CompletionDate = x.CompletionDate }).ToList(), Contacts = customerDto.Contacts?.Select(x =&gt; new Contact() { Id = Guid.NewGuid(), Email = x.Email, Phone = x.Phone }).ToList() }; await _customerRepository.AddAsync(customer); return new CustomerCreatedDto() { Id = customer.Id }; }<p>Мы упростили код транзакционных изменений, теперь мы точно знаем, что сохранение произойдет в транзакции, во всяком случае мы ждем этого от репозитория. В итоге мы сосредоточились на создании объекта в правильном состоянии, в более сложном примере код был больше за счет различных проверок, получения дополнительных данных и т. д.</p>
28 <h3>Реализация для агрегата с логикой</h3>
28 <h3>Реализация для агрегата с логикой</h3>
29 <p>Код можно сделать еще проще, передав логику работы с данными клиента в код класса, таким образом мы сделаем код действительно объектно-ориентированным и получим полноценный агрегат в формате DDD:</p>
29 <p>Код можно сделать еще проще, передав логику работы с данными клиента в код класса, таким образом мы сделаем код действительно объектно-ориентированным и получим полноценный агрегат в формате DDD:</p>
30 /// &lt;summary&gt; /// Клиент /// &lt;/summary&gt; public class Customer { private readonly List&lt;Contact&gt; _contacts = new List&lt;Contact&gt;(); private readonly List&lt;JobPlace&gt; _jobPlaces = new List&lt;JobPlace&gt;(); public Guid Id { get; private set; } public string FullName { get; private set; } public AcquisitionChannel Channel { get; private set; } public DateTime CreatedDate { get; private set; } public bool IsActive { get; private set; } public IEnumerable&lt;JobPlace&gt; JobPlaces =&gt; _jobPlaces.ToList(); public IEnumerable&lt;Contact&gt; Contacts =&gt; _contacts.ToList(); public Customer(string fullName, AcquisitionChannel acquisitionChannel, DateTime createdDate) { Id = Guid.NewGuid(); FullName = fullName; Channel = acquisitionChannel; CreatedDate = createdDate; IsActive = true; } public void AddJobPlaces(List&lt;JobPlace&gt; jobPlaces) { if (jobPlaces != null &amp;&amp; jobPlaces.Any()) { _jobPlaces.AddRange(jobPlaces); } } public void AddContacts(List&lt;Contact&gt; contacts) { if (contacts != null &amp;&amp; contacts.Any()) { _contacts.AddRange(contacts); } } }<p>Мы добавили конструкторы для всех типов, чтобы поместить логику инициализации объекта в одно место и добавили методы заполнения коллекций, также коллекции не являются редактируемыми через интерфейс класса, таким образом вся логика изменения агрегата будет внутри корня агрегации</p>
30 /// &lt;summary&gt; /// Клиент /// &lt;/summary&gt; public class Customer { private readonly List&lt;Contact&gt; _contacts = new List&lt;Contact&gt;(); private readonly List&lt;JobPlace&gt; _jobPlaces = new List&lt;JobPlace&gt;(); public Guid Id { get; private set; } public string FullName { get; private set; } public AcquisitionChannel Channel { get; private set; } public DateTime CreatedDate { get; private set; } public bool IsActive { get; private set; } public IEnumerable&lt;JobPlace&gt; JobPlaces =&gt; _jobPlaces.ToList(); public IEnumerable&lt;Contact&gt; Contacts =&gt; _contacts.ToList(); public Customer(string fullName, AcquisitionChannel acquisitionChannel, DateTime createdDate) { Id = Guid.NewGuid(); FullName = fullName; Channel = acquisitionChannel; CreatedDate = createdDate; IsActive = true; } public void AddJobPlaces(List&lt;JobPlace&gt; jobPlaces) { if (jobPlaces != null &amp;&amp; jobPlaces.Any()) { _jobPlaces.AddRange(jobPlaces); } } public void AddContacts(List&lt;Contact&gt; contacts) { if (contacts != null &amp;&amp; contacts.Any()) { _contacts.AddRange(contacts); } } }<p>Мы добавили конструкторы для всех типов, чтобы поместить логику инициализации объекта в одно место и добавили методы заполнения коллекций, также коллекции не являются редактируемыми через интерфейс класса, таким образом вся логика изменения агрегата будет внутри корня агрегации</p>
31 <p>Код создания клиента ниже:</p>
31 <p>Код создания клиента ниже:</p>
32 public async Task&lt;CustomerCreatedDto&gt; CreateCustomerAsync( CreateCustomerDto customerDto) { if (customerDto == null) throw new ArgumentNullException(nameof(customerDto)); var customer = new Customer(customerDto.FullName, customerDto.Channel, DateTime.Now); var jobPlaces = customerDto.JobPlaces? .Select(x =&gt; new JobPlace(customer, x.Description, x.StartDate, x.CompletionDate )) .ToList(); customer.AddJobPlaces(jobPlaces); var contacts = customerDto.Contacts? .Select(x =&gt; new Contact(customer, x.Email, x.Phone)) .ToList(); customer.AddContacts(contacts); await _customerRepository.AddAsync(customer); return new CustomerCreatedDto() { Id = customer.Id }; }<p>Еще один шаг, который может помочь сделать этот код еще более качественным, заключается в вынесении кода заполнения агрегата по DTO в отдельный класс-фабрику.</p>
32 public async Task&lt;CustomerCreatedDto&gt; CreateCustomerAsync( CreateCustomerDto customerDto) { if (customerDto == null) throw new ArgumentNullException(nameof(customerDto)); var customer = new Customer(customerDto.FullName, customerDto.Channel, DateTime.Now); var jobPlaces = customerDto.JobPlaces? .Select(x =&gt; new JobPlace(customer, x.Description, x.StartDate, x.CompletionDate )) .ToList(); customer.AddJobPlaces(jobPlaces); var contacts = customerDto.Contacts? .Select(x =&gt; new Contact(customer, x.Email, x.Phone)) .ToList(); customer.AddContacts(contacts); await _customerRepository.AddAsync(customer); return new CustomerCreatedDto() { Id = customer.Id }; }<p>Еще один шаг, который может помочь сделать этот код еще более качественным, заключается в вынесении кода заполнения агрегата по DTO в отдельный класс-фабрику.</p>
33 public static class CustomerFactory { public static Customer CreateCustomer(CreateCustomerDto customerDto) { var customer = new Customer(customerDto.FullName, customerDto.Channel, DateTime.Now); var jobPlaces = customerDto.JobPlaces? .Select(x =&gt; new JobPlace(customer, x.Description, x.StartDate, x.CompletionDate )) .ToList(); customer.AddJobPlaces(jobPlaces); var contacts = customerDto.Contacts? .Select(x =&gt; new Contact(customer, x.Email, x.Phone)) .ToList(); customer.AddContacts(contacts); return customer; } }<p>После рефакторинга код изначального сценария будет выглядеть так:</p>
33 public static class CustomerFactory { public static Customer CreateCustomer(CreateCustomerDto customerDto) { var customer = new Customer(customerDto.FullName, customerDto.Channel, DateTime.Now); var jobPlaces = customerDto.JobPlaces? .Select(x =&gt; new JobPlace(customer, x.Description, x.StartDate, x.CompletionDate )) .ToList(); customer.AddJobPlaces(jobPlaces); var contacts = customerDto.Contacts? .Select(x =&gt; new Contact(customer, x.Email, x.Phone)) .ToList(); customer.AddContacts(contacts); return customer; } }<p>После рефакторинга код изначального сценария будет выглядеть так:</p>
34 public async Task&lt;CustomerCreatedDto&gt; CreateCustomerAsync(CreateCustomerDto customerDto) { if (customerDto == null) throw new ArgumentNullException(nameof(customerDto)); var customer = CustomerFactory.CreateCustomer(customerDto); await _customerRepository.AddAsync(customer); return new CustomerCreatedDto() { Id = customer.Id }; }<p>Также агрегат можно улучшить, при необходимости выделив новые объекты-значения, например, если бы поле FullName имело некоторую логику по формированию имени, то его можно было сделать отдельным типом и поместить эту логику туда.</p>
34 public async Task&lt;CustomerCreatedDto&gt; CreateCustomerAsync(CreateCustomerDto customerDto) { if (customerDto == null) throw new ArgumentNullException(nameof(customerDto)); var customer = CustomerFactory.CreateCustomer(customerDto); await _customerRepository.AddAsync(customer); return new CustomerCreatedDto() { Id = customer.Id }; }<p>Также агрегат можно улучшить, при необходимости выделив новые объекты-значения, например, если бы поле FullName имело некоторую логику по формированию имени, то его можно было сделать отдельным типом и поместить эту логику туда.</p>
35 <p>Мы реализовали сценарий и провели последовательный рефакторинг, сосредоточившись на основном смысле сценария, а не на деталях реализации, использовали агрегат для упрощения транзакции и группирования кода, теперь в различных сценариях при расширении логики мы будем руководствоваться идеей расширения агрегата или выделения новых агрегатов, - это приведет к существенному сокращению числа классов с размытой ответственностью.</p>
35 <p>Мы реализовали сценарий и провели последовательный рефакторинг, сосредоточившись на основном смысле сценария, а не на деталях реализации, использовали агрегат для упрощения транзакции и группирования кода, теперь в различных сценариях при расширении логики мы будем руководствоваться идеей расширения агрегата или выделения новых агрегатов, - это приведет к существенному сокращению числа классов с размытой ответственностью.</p>
36 <p>Иногда может потребоваться сделать транзакцию между несколькими агрегатами, возможно, в этом случае мы имеем дело с одним агрегатом и речь идет о неверной декомпозиции или транзакционность для этой операции не является обязательной, тогда полезно задать себе следующие вопросы: "А что бы было если бы данные агрегатов были в разных базах и сервисах? Является ли проблемой то, что данные другого агрегата изменяться в рамках отдельной транзакции?" Если это не является проблемой, то транзакция между агрегатами не нужна, но если они хранятся в одной базе данных, то для уменьшения числа возможных проблем стоит реализовать изменения через единую транзакцию, но скорее всего это будет почти единичный случай, иначе это не должны быть разные агрегаты.</p>
36 <p>Иногда может потребоваться сделать транзакцию между несколькими агрегатами, возможно, в этом случае мы имеем дело с одним агрегатом и речь идет о неверной декомпозиции или транзакционность для этой операции не является обязательной, тогда полезно задать себе следующие вопросы: "А что бы было если бы данные агрегатов были в разных базах и сервисах? Является ли проблемой то, что данные другого агрегата изменяться в рамках отдельной транзакции?" Если это не является проблемой, то транзакция между агрегатами не нужна, но если они хранятся в одной базе данных, то для уменьшения числа возможных проблем стоит реализовать изменения через единую транзакцию, но скорее всего это будет почти единичный случай, иначе это не должны быть разные агрегаты.</p>
37 <h3>Выводы</h3>
37 <h3>Выводы</h3>
38 <p>Мышление в формате агрегатов, а не таблиц, ведет к лучшей модульности операций изменения данных в приложениях, то есть нашей бизнес-логики, код становится более структурированным относительно тразакционных изменений на всех уровнях использования данных, так как и формы пользовательского интерфейса, и API будут неявно следовать этим правилам.</p>
38 <p>Мышление в формате агрегатов, а не таблиц, ведет к лучшей модульности операций изменения данных в приложениях, то есть нашей бизнес-логики, код становится более структурированным относительно тразакционных изменений на всех уровнях использования данных, так как и формы пользовательского интерфейса, и API будут неявно следовать этим правилам.</p>
39 <p>Конечно, взамен мы получаем некоторые сложности с чтением данных в сложных запросах, - эта проблема решается через полноценный CQRS (command-query responsibility segregation) или через реализацию чтения более производительным способом в реализации репозитория, например, чтобы некоторые методы репозитория возвращали не агрегаты, а просто DTO-объекты для конкретной операции чтения. Еще можно получить некоторые проблемы при высокой нагрузке на запись, так как использование агрегатов часто ведет к блокировке нескольких таблиц, но это можно компенсировать через CQRS, убрав операции чтения, если же по логике мы можем писать в разные таблицы, то, возможно, они не являются частью одного агрегата и изначально была сделана неверная декомпозиция. Эти проблемы являются не слишком большой платой за более качественный продукт, который имеет архитектуру, продиктованную требованиями бизнеса и лучше подходит для долгосрочной поддержки сложных сценариев, чем реализация без использования агрегатов.</p>
39 <p>Конечно, взамен мы получаем некоторые сложности с чтением данных в сложных запросах, - эта проблема решается через полноценный CQRS (command-query responsibility segregation) или через реализацию чтения более производительным способом в реализации репозитория, например, чтобы некоторые методы репозитория возвращали не агрегаты, а просто DTO-объекты для конкретной операции чтения. Еще можно получить некоторые проблемы при высокой нагрузке на запись, так как использование агрегатов часто ведет к блокировке нескольких таблиц, но это можно компенсировать через CQRS, убрав операции чтения, если же по логике мы можем писать в разные таблицы, то, возможно, они не являются частью одного агрегата и изначально была сделана неверная декомпозиция. Эти проблемы являются не слишком большой платой за более качественный продукт, который имеет архитектуру, продиктованную требованиями бизнеса и лучше подходит для долгосрочной поддержки сложных сценариев, чем реализация без использования агрегатов.</p>
40  
40