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
/// <summary> /// Клиент /// </summary> public class Customer { /// <summary> /// Id /// </summary> public Guid Id { get; set; } /// <summary> /// Полное имя клиента /// </summary> public string FullName { get; set; } /// <summary> /// Канал привлечения (интернет-реклама, реклама на улице и т.д.) /// </summary> public AcquisitionChannel Channel { get; set; } /// <summary> /// Дата создания /// </summary> public DateTime CreatedDate { get; set; } /// <summary> /// Признак активности /// </summary> public bool IsActive { get; set; } } /// <summary> /// Место работы /// </summary> public class JobPlace { /// <summary> /// Id, уникальный идентификатор /// </summary> public Guid Id { get; set; } /// <summary> /// Описание места работы /// </summary> public string Description { get; set; } /// <summary> /// Дата начала работы /// </summary> public DateTime StartDate { get; set; } /// <summary> /// Дата окончания работы /// </summary> public DateTime? CompletionDate { get; set; } /// <summary> /// Идентификатор клиента /// </summary> public Guid CustomerId { get; set; } } /// <summary> /// Контакт клиента /// </summary> public class Contact { /// <summary> /// Id, уникальный идентификатор /// </summary> public Guid Id { get; set; } /// <summary> /// Адрес электронной почты /// </summary> public string Email { get; set; } /// <summary> /// Телефон /// </summary> public string Phone { get; set; } /// <summary> /// Id клиента /// </summary> public Guid CustomerId { get; set; } }<p>Для работы с базой данных создадим Generic-репозиторий с интерфейсом ниже, так как базового набора операций для каждой таблицы хватит:</p>
11
/// <summary> /// Клиент /// </summary> public class Customer { /// <summary> /// Id /// </summary> public Guid Id { get; set; } /// <summary> /// Полное имя клиента /// </summary> public string FullName { get; set; } /// <summary> /// Канал привлечения (интернет-реклама, реклама на улице и т.д.) /// </summary> public AcquisitionChannel Channel { get; set; } /// <summary> /// Дата создания /// </summary> public DateTime CreatedDate { get; set; } /// <summary> /// Признак активности /// </summary> public bool IsActive { get; set; } } /// <summary> /// Место работы /// </summary> public class JobPlace { /// <summary> /// Id, уникальный идентификатор /// </summary> public Guid Id { get; set; } /// <summary> /// Описание места работы /// </summary> public string Description { get; set; } /// <summary> /// Дата начала работы /// </summary> public DateTime StartDate { get; set; } /// <summary> /// Дата окончания работы /// </summary> public DateTime? CompletionDate { get; set; } /// <summary> /// Идентификатор клиента /// </summary> public Guid CustomerId { get; set; } } /// <summary> /// Контакт клиента /// </summary> public class Contact { /// <summary> /// Id, уникальный идентификатор /// </summary> public Guid Id { get; set; } /// <summary> /// Адрес электронной почты /// </summary> public string Email { get; set; } /// <summary> /// Телефон /// </summary> public string Phone { get; set; } /// <summary> /// Id клиента /// </summary> public Guid CustomerId { get; set; } }<p>Для работы с базой данных создадим Generic-репозиторий с интерфейсом ниже, так как базового набора операций для каждой таблицы хватит:</p>
12
public interface IRepository<T> { Task<IEnumerable<T>> GetAllAsync(); Task<T> GetByIdAsync(Guid id); Task<Guid> AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(T entityId); }<p>В реализации будет код для работы с SQL-базой данных, это могут быть SQL-вызовы базы данных или вызовы хранимых процедур, функций или представлений.</p>
12
public interface IRepository<T> { Task<IEnumerable<T>> GetAllAsync(); Task<T> GetByIdAsync(Guid id); Task<Guid> 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<CustomerCreatedDto> 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 => new JobPlace() { Id = Guid.NewGuid(), CustomerId = customerId, Description = x.Description, StartDate = x.StartDate, CompletionDate = x.CompletionDate }).ToList(); if (jobPlaces != null && jobPlaces.Any()) { foreach (var jobPlace in jobPlaces) { await _jobPlaceRepository.AddAsync(jobPlace); } } var contacts = customerDto.Contacts?.Select(x => new Contact() { Id = Guid.NewGuid(), CustomerId = customerId, Email = x.Email, Phone = x.Phone }).ToList(); if (contacts != null && 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<CustomerCreatedDto> 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 => new JobPlace() { Id = Guid.NewGuid(), CustomerId = customerId, Description = x.Description, StartDate = x.StartDate, CompletionDate = x.CompletionDate }).ToList(); if (jobPlaces != null && jobPlaces.Any()) { foreach (var jobPlace in jobPlaces) { await _jobPlaceRepository.AddAsync(jobPlace); } } var contacts = customerDto.Contacts?.Select(x => new Contact() { Id = Guid.NewGuid(), CustomerId = customerId, Email = x.Email, Phone = x.Phone }).ToList(); if (contacts != null && 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
/// <summary> /// Клиент /// </summary> 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<JobPlace> JobPlaces { get; set; } public List<Contact> Contacts { get; set; } }<p>На первый взгляд, почти ничего не изменилось, но у нас появились поля для доступа коллекциям связанных объектов.</p>
23
/// <summary> /// Клиент /// </summary> 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<JobPlace> JobPlaces { get; set; } public List<Contact> 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<CustomerCreatedDto> 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 => new JobPlace() { Id = Guid.NewGuid(), Description = x.Description, StartDate = x.StartDate, CompletionDate = x.CompletionDate }).ToList(), Contacts = customerDto.Contacts?.Select(x => 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<CustomerCreatedDto> 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 => new JobPlace() { Id = Guid.NewGuid(), Description = x.Description, StartDate = x.StartDate, CompletionDate = x.CompletionDate }).ToList(), Contacts = customerDto.Contacts?.Select(x => 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
/// <summary> /// Клиент /// </summary> public class Customer { private readonly List<Contact> _contacts = new List<Contact>(); private readonly List<JobPlace> _jobPlaces = new List<JobPlace>(); 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<JobPlace> JobPlaces => _jobPlaces.ToList(); public IEnumerable<Contact> Contacts => _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<JobPlace> jobPlaces) { if (jobPlaces != null && jobPlaces.Any()) { _jobPlaces.AddRange(jobPlaces); } } public void AddContacts(List<Contact> contacts) { if (contacts != null && contacts.Any()) { _contacts.AddRange(contacts); } } }<p>Мы добавили конструкторы для всех типов, чтобы поместить логику инициализации объекта в одно место и добавили методы заполнения коллекций, также коллекции не являются редактируемыми через интерфейс класса, таким образом вся логика изменения агрегата будет внутри корня агрегации</p>
30
/// <summary> /// Клиент /// </summary> public class Customer { private readonly List<Contact> _contacts = new List<Contact>(); private readonly List<JobPlace> _jobPlaces = new List<JobPlace>(); 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<JobPlace> JobPlaces => _jobPlaces.ToList(); public IEnumerable<Contact> Contacts => _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<JobPlace> jobPlaces) { if (jobPlaces != null && jobPlaces.Any()) { _jobPlaces.AddRange(jobPlaces); } } public void AddContacts(List<Contact> contacts) { if (contacts != null && contacts.Any()) { _contacts.AddRange(contacts); } } }<p>Мы добавили конструкторы для всех типов, чтобы поместить логику инициализации объекта в одно место и добавили методы заполнения коллекций, также коллекции не являются редактируемыми через интерфейс класса, таким образом вся логика изменения агрегата будет внутри корня агрегации</p>
31
<p>Код создания клиента ниже:</p>
31
<p>Код создания клиента ниже:</p>
32
public async Task<CustomerCreatedDto> 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 => new JobPlace(customer, x.Description, x.StartDate, x.CompletionDate )) .ToList(); customer.AddJobPlaces(jobPlaces); var contacts = customerDto.Contacts? .Select(x => 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<CustomerCreatedDto> 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 => new JobPlace(customer, x.Description, x.StartDate, x.CompletionDate )) .ToList(); customer.AddJobPlaces(jobPlaces); var contacts = customerDto.Contacts? .Select(x => 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 => new JobPlace(customer, x.Description, x.StartDate, x.CompletionDate )) .ToList(); customer.AddJobPlaces(jobPlaces); var contacts = customerDto.Contacts? .Select(x => 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 => new JobPlace(customer, x.Description, x.StartDate, x.CompletionDate )) .ToList(); customer.AddJobPlaces(jobPlaces); var contacts = customerDto.Contacts? .Select(x => new Contact(customer, x.Email, x.Phone)) .ToList(); customer.AddContacts(contacts); return customer; } }<p>После рефакторинга код изначального сценария будет выглядеть так:</p>
34
public async Task<CustomerCreatedDto> 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<CustomerCreatedDto> 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