День 1127. #DDD
Раскрытие Реализации в Именах Классов
Эта тема возникла в ответ на вопрос о паттерне CQRS, но он в равной степени применима и к другим паттернам. Вопрос:
Почему DatabaseRetryDecorator раскрывает свою структуру и детали реализации в имени класса? Что, если я изменю реализацию и заменю декоратор чем-то другим? Придется ли мне переименовывать его только потому, что я изменил его реализацию?
Просто для контекста, декораторы — это классы, которые позволяют вам вводить сквозную функциональность в вашем приложении.
Вот пример использования декоратора для повторных попыток при обращении к базе данных:
Действительно, почему вы должны называть класс
Здесь есть 2 совета.
1. Никогда не используйте имена паттернов при именовании классов на уровне предметной области (Domain layer). Вы должны использовать только термины из общего языка. Так что, не
2. Допустимо использовать имена паттернов при именовании классов на прикладном уровне (Application layer), потому что прикладной уровень находится за пределами действия общего языка.
Кроме того, имена паттернов помогают лучше понять назначение классов на прикладном уровне, поскольку эти классы не имеют прямой связи с вашей моделью предметной области.
Обратите внимание, что вы всегда должны следовать принципу YAGNI, даже при именовании классов прикладного уровня. Это означает, что вы должны поместить минимальное количество дескрипторов в имена классов.
Самый распространенный пример нарушения YAGNI — вызов репозитория
Источник: https://khorikov.org/posts/2021-08-16-exposing-implementation-details-in-class-names/
Раскрытие Реализации в Именах Классов
Эта тема возникла в ответ на вопрос о паттерне CQRS, но он в равной степени применима и к другим паттернам. Вопрос:
Почему DatabaseRetryDecorator раскрывает свою структуру и детали реализации в имени класса? Что, если я изменю реализацию и заменю декоратор чем-то другим? Придется ли мне переименовывать его только потому, что я изменил его реализацию?
Просто для контекста, декораторы — это классы, которые позволяют вам вводить сквозную функциональность в вашем приложении.
Вот пример использования декоратора для повторных попыток при обращении к базе данных:
[DatabaseRetry]Приведенный выше атрибут
public class EditPersonalInfoCommandHandler :
ICommandHandler<EditPersonalInfoCommand>
{
public Result Handle(EditPersonalInfoCommand command)
{
/* Изменяем данные клиента */
}
}
DatabaseRetryAttribute соединяет обработчик команд с DatabaseRetryDecorator. Если обработчик команды выдаёт исключение базы данных, декоратор повторно его запускает (как вариант, через некоторое время). Идея аналогична тому, как промежуточное ПО работает в ASP.NET.Действительно, почему вы должны называть класс
DatabaseRetryDecorator, а не просто DatabaseRetry? Тот же аргумент можно применить и к другим классам, например, CustomerRepository, CustomerFactory или даже к EditPersonalInfoCommandHandler. Так ли им нужно указывать на паттерны, которые они реализуют, в своих именах (репозиторий, фабрика и обработчик команд)?Здесь есть 2 совета.
1. Никогда не используйте имена паттернов при именовании классов на уровне предметной области (Domain layer). Вы должны использовать только термины из общего языка. Так что, не
CustomerEntity или CustomerAggregate, а просто Customer.2. Допустимо использовать имена паттернов при именовании классов на прикладном уровне (Application layer), потому что прикладной уровень находится за пределами действия общего языка.
Кроме того, имена паттернов помогают лучше понять назначение классов на прикладном уровне, поскольку эти классы не имеют прямой связи с вашей моделью предметной области.
Обратите внимание, что вы всегда должны следовать принципу YAGNI, даже при именовании классов прикладного уровня. Это означает, что вы должны поместить минимальное количество дескрипторов в имена классов.
Самый распространенный пример нарушения YAGNI — вызов репозитория
CustomerSqlRepository. Если вы не используете несколько парадигм хранения, таких как SQL и NoSQL, нет необходимости указывать, что ваш репозиторий работает поверх базы данных SQL.Источник: https://khorikov.org/posts/2021-08-16-exposing-implementation-details-in-class-names/
👍9
День 1133. #DDD
Коллекции и Одержимость Примитивными Типами. Начало
Одержимость примитивными типами (Primitive Obsession) — это антипаттерн, который заключается в чрезмерном использовании примитивных типов, особенно для моделирования предметной области.
Этот антипаттерн широко известен как в сообществах DDD, так и в сообществах функционального программирования. В DDD вместо этого есть паттерн объекта-значения, а в функциональном программировании существует культура введения типов-оболочек для любого небольшого понятия из вашей предметной области, прежде всего потому, что эти типы-оболочки очень легко создавать в функциональных языках.
Обычно одержимость примитивными типами проявляется в использовании строк, целых чисел и других простых типов. Распространённые примеры:
- Использование строки для представления адресов электронной почты. Специально созданный объект-значение
- Использование
Конечно, не все концепции в вашей предметной области должны быть представлены как объекты-значения, необходимо провести некоторый анализ, чтобы выяснить, стоит ли создавать новый объект-значение. Но недостаточное использование объектов-значений — гораздо более серьёзная проблема, чем чрезмерное их использование.
Но зачем нам вообще нужны объекты-значения?
Дело в инкапсуляции. Она предназначена для защиты данных от перехода в недопустимое состояние. Гораздо сложнее защитить от этого строку, чем специально созданный класс со всеми необходимыми встроенными проверками.
Действительная строка может быть или не быть действительным адресом электронной почты; концепция строки слишком широка и сама по себе не может учитывать правила валидности электронного адреса.
Другими словами, множество строк больше, чем множество e-mail адресов. Чтобы правильно представить концепцию e-mail адреса, нам нужно создать пользовательский класс, который будет соответствовать множеству действительных e-mail адресов.
Помимо инкапсуляции, есть ещё принцип абстракции. Класс
Окончание следует…
Источник: https://enterprisecraftsmanship.com/posts/collections-primitive-obsession/
Коллекции и Одержимость Примитивными Типами. Начало
Одержимость примитивными типами (Primitive Obsession) — это антипаттерн, который заключается в чрезмерном использовании примитивных типов, особенно для моделирования предметной области.
Этот антипаттерн широко известен как в сообществах DDD, так и в сообществах функционального программирования. В DDD вместо этого есть паттерн объекта-значения, а в функциональном программировании существует культура введения типов-оболочек для любого небольшого понятия из вашей предметной области, прежде всего потому, что эти типы-оболочки очень легко создавать в функциональных языках.
Обычно одержимость примитивными типами проявляется в использовании строк, целых чисел и других простых типов. Распространённые примеры:
- Использование строки для представления адресов электронной почты. Специально созданный объект-значение
Email был бы здесь намного лучше.- Использование
double для моделирования концепции денег вместо введения объекта-значения Money.Конечно, не все концепции в вашей предметной области должны быть представлены как объекты-значения, необходимо провести некоторый анализ, чтобы выяснить, стоит ли создавать новый объект-значение. Но недостаточное использование объектов-значений — гораздо более серьёзная проблема, чем чрезмерное их использование.
Но зачем нам вообще нужны объекты-значения?
Дело в инкапсуляции. Она предназначена для защиты данных от перехода в недопустимое состояние. Гораздо сложнее защитить от этого строку, чем специально созданный класс со всеми необходимыми встроенными проверками.
Действительная строка может быть или не быть действительным адресом электронной почты; концепция строки слишком широка и сама по себе не может учитывать правила валидности электронного адреса.
Другими словами, множество строк больше, чем множество e-mail адресов. Чтобы правильно представить концепцию e-mail адреса, нам нужно создать пользовательский класс, который будет соответствовать множеству действительных e-mail адресов.
Помимо инкапсуляции, есть ещё принцип абстракции. Класс
Email абстрагирует все бизнес-правила, связанные с e-mail адресами, так что клиентский код может работать с этими объектами, не обращая внимания на детали реализации, связанные с проверкой e-mail.Окончание следует…
Источник: https://enterprisecraftsmanship.com/posts/collections-primitive-obsession/
👍16
День 1134. #DDD
Коллекции и Одержимость Примитивными Типами. Окончание
Начало
Пользовательские классы коллекций
А что насчёт коллекций? Допустим, у нас есть следующий класс:
Рассмотрим примеры выше.
1. Коллекция сущностей
Нам нужно, чтобы свойство
2. Пользовательский класс коллекции
Если коллекция не принадлежит никакой другой сущности, то имеет смысл создать для неё отдельный класс. Например, если вам нужно отслеживать всех пользователей, которые в данный момент находятся в сети, лучше всего представить их в виде пользовательского класса:
Итого
Одержимость примитивными типами — это использование примитивных типов для моделирования предметной области.
Вам может понадобиться или не понадобиться отдельный класс для коллекции. Если коллекция представляет собой набор связанных сущностей, присоединённых к родительской сущности, то эта родительская сущность фактически действует как пользовательский класс. Отдельный класс для самой коллекции не нужен.
Если же коллекция является коллекцией корневого уровня, то для неё нужен пользовательский класс (при условии, что помимо самой коллекции необходимы дополнительные функции).
Источник: https://enterprisecraftsmanship.com/posts/collections-primitive-obsession/
Коллекции и Одержимость Примитивными Типами. Окончание
Начало
Пользовательские классы коллекций
А что насчёт коллекций? Допустим, у нас есть следующий класс:
public class CustomerМожет коллекция должна быть представлена специальным классом, например:
{
public IReadOnlyList<Order> Orders { get; }
}
public class CustomerЭто зависит. Если в коллекции необходимо реализовать дополнительные бизнес-правила или инварианты, может быть хорошей идеей создать пользовательский класс. В противном случае оно того не стоит.
{
public OrderList Orders { get; }
}
Рассмотрим примеры выше.
1. Коллекция сущностей
public IReadOnlyList<Order> Orders { get; }
Какие бизнес-правила могут быть реализованы в этой коллекции? Допустим, не может быть повторяющихся заказов. Требует ли это введения нового класса? Нет. Чтобы соблюсти это бизнес-правило, все новые заказы должны проходить валидацию. Но нам не нужен для этого отдельный класс, эту ответственность может взять на себя сам класс Customer.Нам нужно, чтобы свойство
Orders не могло быть изменено напрямую, а это уже сделано путем представления этого свойства как IReadOnlyList. Значит нужно изменить класс Customer, добавив метод с необходимой проверкой:public class CustomerВ некотором роде
{
private List<Order> _orders;
public IReadOnlyList<Order> Orders => _orders;
public void AddOrder(Order order)
{
if (_orders.Contains(order))
throw new Exception();
_orders.Add(order);
}
}
Customer уже представляет собой пользовательский класс, инкапсулирующий коллекцию Orders.2. Пользовательский класс коллекции
Если коллекция не принадлежит никакой другой сущности, то имеет смысл создать для неё отдельный класс. Например, если вам нужно отслеживать всех пользователей, которые в данный момент находятся в сети, лучше всего представить их в виде пользовательского класса:
public class OnlineUsersЗдесь подразумевается, что, помимо собственно коллекции, необходимы некоторые дополнительные функции (например, описанный выше метод
{
private List<User> _users;
public void ForceLogOff(long userId)
{
// …
}
}
ForceLogOff), в противном случае класс OnlineUsers не нужен.Итого
Одержимость примитивными типами — это использование примитивных типов для моделирования предметной области.
Вам может понадобиться или не понадобиться отдельный класс для коллекции. Если коллекция представляет собой набор связанных сущностей, присоединённых к родительской сущности, то эта родительская сущность фактически действует как пользовательский класс. Отдельный класс для самой коллекции не нужен.
Если же коллекция является коллекцией корневого уровня, то для неё нужен пользовательский класс (при условии, что помимо самой коллекции необходимы дополнительные функции).
Источник: https://enterprisecraftsmanship.com/posts/collections-primitive-obsession/
👍14
День 1237. #ЗаметкиНаПолях #DDD
Сущности и Объекты-Значения: Полный Список Различий. Начало
Тема не новая, тем не менее, здесь все различия сведены вместе.
1. Типы равенства
Для начала рассмотрим 3 типа равенства, которые имеют значение, когда нам нужно сравнивать объекты друг с другом.
- Ссылочное равенство: два объекта считаются равными, если они ссылаются на один и тот же адрес в памяти.
Вот как мы можем проверить это в коде:
- Структурное равенство: два объекта считаются равными, если все их члены равны.
Основное различие между сущностями и объектами-значениями заключается в том, как мы сравниваем их экземпляры друг с другом. Понятие равенства идентификаторов относится к сущностям, тогда как понятие структурного равенства — к объектам-значениям. Т.е. сущности обладают внутренней идентичностью, а объекты-значения — нет, и если два объекта-значения имеют одинаковый набор атрибутов, мы можем рассматривать их как взаимозаменяемые. В то же время, если данные в двух экземплярах сущности совпадают (кроме свойства
У двух людей может быть одно имя или даже фамилия. Но вы не считаете их одним человеком, т.к. у каждого собственная внутренняя идентичность. Однако, если у вас есть купюра в 1 доллар, вам всё равно, какая именно это купюра. Вы можете заменить одну на другую с тем же номиналом, таким образом, это объект-значения.
2. Время жизни
Сущности живут долго, у них есть история (даже если она не хранится) о том, что с ними произошло и как они изменялись за свою жизнь. Объекты-значения, напротив, имеют нулевую продолжительность жизни. Они легко создаются и уничтожаются. Это следствие их взаимозаменяемости.
Таким образом, объекты-значения не могут жить сами по себе, они всегда должны принадлежать одной или нескольким сущностям. Данные, которые представляет объект-значение, имеют смысл только в контексте объекта, который на них ссылается. В примере выше вопрос "Сколько денег?" не имеет смысла, потому что не передаёт надлежащего контекста. Тогда как, вопросы "Сколько денег у пользователя X?" или "Сколько денег у всех пользователей?" совершенно валидны.
Ещё одно следствие: объекты-значения не хранятся отдельно. Единственный способ сохранить объект-значение — это присоединить его к сущности.
3. Неизменяемость
Объекты-значения должны быть неизменяемыми в том смысле, что, если нам нужно изменить такой объект, мы создаём новый экземпляр на основе существующего объекта, а не изменяем его. Напротив, сущности почти всегда изменяемы.
Вопрос неизменяемости давно является предметом спора. Некоторые утверждают, что это правило не такое строгое, и в некоторых случаях объекты-значения могут изменяться. Однако, изменяя экземпляр объекта-значения, вы предполагаете, что у него есть собственный жизненный цикл. И это допущение, в свою очередь, приводит к заключению, что объект-значение имеет свою собственную неотъемлемую идентичность, что противоречит определению этого понятия в DDD.
Объекты-значения имеют нулевое время жизни, т.е. являются просто моментальными снимками некоторого состояния и не более того, поэтому им разрешено представлять только один вариант этого состояния.
Это приводит нас к следующему эмпирическому правилу: если вы не можете сделать объект-значение неизменяемым, то это не объект-значение. Хотя обратное не всегда верно. Сущности в некоторых случаях могут быть неизменяемыми в вашем домене.
Окончание следует…
Источник: https://enterprisecraftsmanship.com/posts/entity-vs-value-object-the-ultimate-list-of-differences
Сущности и Объекты-Значения: Полный Список Различий. Начало
Тема не новая, тем не менее, здесь все различия сведены вместе.
1. Типы равенства
Для начала рассмотрим 3 типа равенства, которые имеют значение, когда нам нужно сравнивать объекты друг с другом.
- Ссылочное равенство: два объекта считаются равными, если они ссылаются на один и тот же адрес в памяти.
Вот как мы можем проверить это в коде:
var obj1 = new object();- Равенство идентификаторов подразумевает, что у класса есть поле
var obj2 = obj1;
bool areEqual = object.ReferenceEquals(obj1, obj2);
id. Два экземпляра такого класса будут равны, если они имеют одинаковые идентификаторы.- Структурное равенство: два объекта считаются равными, если все их члены равны.
Основное различие между сущностями и объектами-значениями заключается в том, как мы сравниваем их экземпляры друг с другом. Понятие равенства идентификаторов относится к сущностям, тогда как понятие структурного равенства — к объектам-значениям. Т.е. сущности обладают внутренней идентичностью, а объекты-значения — нет, и если два объекта-значения имеют одинаковый набор атрибутов, мы можем рассматривать их как взаимозаменяемые. В то же время, если данные в двух экземплярах сущности совпадают (кроме свойства
Id), мы не считаем их эквивалентными.У двух людей может быть одно имя или даже фамилия. Но вы не считаете их одним человеком, т.к. у каждого собственная внутренняя идентичность. Однако, если у вас есть купюра в 1 доллар, вам всё равно, какая именно это купюра. Вы можете заменить одну на другую с тем же номиналом, таким образом, это объект-значения.
2. Время жизни
Сущности живут долго, у них есть история (даже если она не хранится) о том, что с ними произошло и как они изменялись за свою жизнь. Объекты-значения, напротив, имеют нулевую продолжительность жизни. Они легко создаются и уничтожаются. Это следствие их взаимозаменяемости.
Таким образом, объекты-значения не могут жить сами по себе, они всегда должны принадлежать одной или нескольким сущностям. Данные, которые представляет объект-значение, имеют смысл только в контексте объекта, который на них ссылается. В примере выше вопрос "Сколько денег?" не имеет смысла, потому что не передаёт надлежащего контекста. Тогда как, вопросы "Сколько денег у пользователя X?" или "Сколько денег у всех пользователей?" совершенно валидны.
Ещё одно следствие: объекты-значения не хранятся отдельно. Единственный способ сохранить объект-значение — это присоединить его к сущности.
3. Неизменяемость
Объекты-значения должны быть неизменяемыми в том смысле, что, если нам нужно изменить такой объект, мы создаём новый экземпляр на основе существующего объекта, а не изменяем его. Напротив, сущности почти всегда изменяемы.
Вопрос неизменяемости давно является предметом спора. Некоторые утверждают, что это правило не такое строгое, и в некоторых случаях объекты-значения могут изменяться. Однако, изменяя экземпляр объекта-значения, вы предполагаете, что у него есть собственный жизненный цикл. И это допущение, в свою очередь, приводит к заключению, что объект-значение имеет свою собственную неотъемлемую идентичность, что противоречит определению этого понятия в DDD.
Объекты-значения имеют нулевое время жизни, т.е. являются просто моментальными снимками некоторого состояния и не более того, поэтому им разрешено представлять только один вариант этого состояния.
Это приводит нас к следующему эмпирическому правилу: если вы не можете сделать объект-значение неизменяемым, то это не объект-значение. Хотя обратное не всегда верно. Сущности в некоторых случаях могут быть неизменяемыми в вашем домене.
Окончание следует…
Источник: https://enterprisecraftsmanship.com/posts/entity-vs-value-object-the-ultimate-list-of-differences
👍16
День 1238. #ЗаметкиНаПолях #DDD
Сущности и Объекты-Значения: Полный Список Различий. Окончание
Начало
Как распознать объект-значение в вашей модели предметной области?
К сожалению, нет никаких объективных признаков, по которым вы могли бы узнать, это. Является ли понятие объектом-значением или нет, полностью зависит от предметной области: понятие может быть сущностью в одной модели предметной области и объектом-значением в другой.
В примере из предыдущего поста мы относимся к деньгам взаимозаменяемо, что делает это понятие объектом-значением. В то же время, если мы создадим программу для отслеживания потоков наличных, нам нужно будет обрабатывать каждую отдельную купюру отдельно, чтобы собирать статистику по каждой из них. В этом случае понятие денег было бы сущностью, которую мы, вероятно, назвали бы Note (Банкнота).
Если вы можете безопасно заменить экземпляр класса другим экземпляром с таким же набором атрибутов, это хороший признак того, что перед вами объект-значения. Можно сравнить объект-значения с числом. Вам не важно является ли число 5 тем же числом 5, которое вы использовали в другом методе. Т.е. это объект-значение. Если ваше понятие ведёт себя как число, то это объект-значение.
Как хранить объекты-значения в БД?
Допустим, у нас есть два класса в нашей модели предметной области: сущность
1)
2) Мы потенциально можем отделить объекты-значения от сущностей.
*Здесь надо заметить, что ORM системы, вроде Entity Framework, позволяют сохранять объекты-значения в отдельные таблицы, сохраняя при этом абстракцию объекта-значения.
Предпочитайте объекты-значения сущностям
Всегда отдавайте предпочтение объектам-значениям, а не сущностям. Объекты-значения являются неизменяемыми и более лёгкими, чем сущности, поэтому с ними легче работать. В идеале вы должны помещать большую часть бизнес-логики в объекты-значения. Сущности будут действовать как оболочки для них и представлять более высокоуровневую функциональность.
Может случиться, что то, что вы сначала рассматривали, как сущность, по сути является объектом-значением. В этом случае не стесняйтесь реорганизовать модель предметной области и преобразовать сущность в объект-значение.
Итого
- Сущности имеют свою внутреннюю идентичность, а объекты-значения — нет.
- Понятие равенства идентификаторов относится к сущностям; понятие структурного равенства относится к объекты-значениям; понятие ссылочного равенства относится к обоим.
- Сущности имеют историю; объекты-значения имеют нулевую продолжительность жизни.
- Объект-значение всегда должен принадлежать одной или нескольким сущностям, он не может жить сам по себе.
- Объекты-значения должны быть неизменяемыми; сущности почти всегда изменяемы.
- Чтобы распознать объект-значение в модели предметной области, мысленно замените его числом.
- Всегда отдавайте предпочтение объектам-значениям, а не сущностям в вашей модели предметной области.
Источник: https://enterprisecraftsmanship.com/posts/entity-vs-value-object-the-ultimate-list-of-differences
Сущности и Объекты-Значения: Полный Список Различий. Окончание
Начало
Как распознать объект-значение в вашей модели предметной области?
К сожалению, нет никаких объективных признаков, по которым вы могли бы узнать, это. Является ли понятие объектом-значением или нет, полностью зависит от предметной области: понятие может быть сущностью в одной модели предметной области и объектом-значением в другой.
В примере из предыдущего поста мы относимся к деньгам взаимозаменяемо, что делает это понятие объектом-значением. В то же время, если мы создадим программу для отслеживания потоков наличных, нам нужно будет обрабатывать каждую отдельную купюру отдельно, чтобы собирать статистику по каждой из них. В этом случае понятие денег было бы сущностью, которую мы, вероятно, назвали бы Note (Банкнота).
Если вы можете безопасно заменить экземпляр класса другим экземпляром с таким же набором атрибутов, это хороший признак того, что перед вами объект-значения. Можно сравнить объект-значения с числом. Вам не важно является ли число 5 тем же числом 5, которое вы использовали в другом методе. Т.е. это объект-значение. Если ваше понятие ведёт себя как число, то это объект-значение.
Как хранить объекты-значения в БД?
Допустим, у нас есть два класса в нашей модели предметной области: сущность
Person и объект значения Address:// СущностьОдин из вариантов хранения: добавить в
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public Address Address { get; set; }
}
// Объект-значение
public class Address
{
public string City { get; set; }
public string ZipCode { get; set; }
}
Address поле Id и хранить в отдельной таблице. Это правильно с точки зрения БД, но имеет два недостатка:1)
Address содержит идентификатор, т.е. мы предоставляем классу Address некоторую идентичность. И это нарушает определение объекта-значения.2) Мы потенциально можем отделить объекты-значения от сущностей.
Address теперь может жить сам по себе, потому что мы можем удалить строку Person, не удаляя соответствующую строку Address. Кроме того, вы можете выбрать объект Address из таблицы отдельно от объекта Person. Это нарушило бы другое правило, согласно которому время жизни объектов-значений должно полностью зависеть от времени жизни их родительских сущностей. Получается, лучшее решение* — встроить поля из таблицы Address в таблицу Person.*Здесь надо заметить, что ORM системы, вроде Entity Framework, позволяют сохранять объекты-значения в отдельные таблицы, сохраняя при этом абстракцию объекта-значения.
Предпочитайте объекты-значения сущностям
Всегда отдавайте предпочтение объектам-значениям, а не сущностям. Объекты-значения являются неизменяемыми и более лёгкими, чем сущности, поэтому с ними легче работать. В идеале вы должны помещать большую часть бизнес-логики в объекты-значения. Сущности будут действовать как оболочки для них и представлять более высокоуровневую функциональность.
Может случиться, что то, что вы сначала рассматривали, как сущность, по сути является объектом-значением. В этом случае не стесняйтесь реорганизовать модель предметной области и преобразовать сущность в объект-значение.
Итого
- Сущности имеют свою внутреннюю идентичность, а объекты-значения — нет.
- Понятие равенства идентификаторов относится к сущностям; понятие структурного равенства относится к объекты-значениям; понятие ссылочного равенства относится к обоим.
- Сущности имеют историю; объекты-значения имеют нулевую продолжительность жизни.
- Объект-значение всегда должен принадлежать одной или нескольким сущностям, он не может жить сам по себе.
- Объекты-значения должны быть неизменяемыми; сущности почти всегда изменяемы.
- Чтобы распознать объект-значение в модели предметной области, мысленно замените его числом.
- Всегда отдавайте предпочтение объектам-значениям, а не сущностям в вашей модели предметной области.
Источник: https://enterprisecraftsmanship.com/posts/entity-vs-value-object-the-ultimate-list-of-differences
👍9
День 1254. #DDD
Обязанности Агрегатов в DDD
Агрегат происходит из DDD и предоставляет способ инкапсулировать бизнес-логику в нескольких связанных объектах.
Основные правила агрегатов:
1. Каждый агрегат имеет один корневой объект. Агрегаты называются по их корневому объекту.
2. Дочерние элементы агрегатов не сохраняются отдельно, а только как часть агрегата. Более того, доступ к ним также должен осуществляться только через агрегат.
Например, навигационные свойства должны существовать только от корней агрегатов к дочерним элементам (но не наоборот и не между агрегатами). Т.е. корень должен иметь возможность обращаться к любому из своих дочерних элементов, а дочерние элементы обычно имеют только свойство ID своего родителя.
3. Корень отвечает за валидность всего агрегата
Это не означает, что весь код должен находиться в корневом объекте. Корень может делегировать полномочия своим дочерним элементам при выполнении задач, за которые он отвечает.
Рассмотрим простой пример сущности
- создание нового заказа,
- добавление
- изменение количества в
Некоторые бизнес-правила:
- валидный заказ имеет хотя бы один
- валидная коллекция
- валидный
Очевидно, что
Многие разработчики также считают, что правило агрегата, гарантирующее, что весь агрегат является «действительным», требует метода для проверки достоверности. Иногда это полезно, но лучший дизайн — это тот, который изначально не допускает существования недопустимых значений. Снова рассмотрим тип
Итого
Корень агрегата отвечает за то, чтобы агрегат находился в допустимом состоянии. Но это не означает, что каждая операция, выполняемая над любым элементом агрегата, должна находиться в корне агрегата. Это также не означает, что агрегат должен иметь метод, выполняющий проверку. Использование надлежащего объектно-ориентированного дизайна и обеспечение того, чтобы ваши объекты инкапсулировали и управляли своим собственным состоянием, обеспечивает более чистый дизайн.
См. также Сущности и Объекты-Значения
Источник: https://ardalis.com/aggregate-responsibility-design/
Обязанности Агрегатов в DDD
Агрегат происходит из DDD и предоставляет способ инкапсулировать бизнес-логику в нескольких связанных объектах.
Основные правила агрегатов:
1. Каждый агрегат имеет один корневой объект. Агрегаты называются по их корневому объекту.
2. Дочерние элементы агрегатов не сохраняются отдельно, а только как часть агрегата. Более того, доступ к ним также должен осуществляться только через агрегат.
Например, навигационные свойства должны существовать только от корней агрегатов к дочерним элементам (но не наоборот и не между агрегатами). Т.е. корень должен иметь возможность обращаться к любому из своих дочерних элементов, а дочерние элементы обычно имеют только свойство ID своего родителя.
3. Корень отвечает за валидность всего агрегата
Это не означает, что весь код должен находиться в корневом объекте. Корень может делегировать полномочия своим дочерним элементам при выполнении задач, за которые он отвечает.
Рассмотрим простой пример сущности
Order, которая имеет коллекцию сущностей OrderItem. Order — корень:public class OrderНекоторые типичные операции агрегата:
{
// прочие свойства и методы опущены
public IEnumerable<OrderItem> OrderItems { get; private set;}
}
public class OrderItem
{
// прочие свойства и методы опущены
public int ProductId { get; set; }
public int Quantity { get; set; }
}
- создание нового заказа,
- добавление
OrderItem в заказ,- изменение количества в
OrderItem.Некоторые бизнес-правила:
- валидный заказ имеет хотя бы один
OrderItem,- валидная коллекция
OrderItems не содержит повторяющихся ProductId,- валидный
OrderItem имеет положительное количество.Очевидно, что
Order здесь отвечает за первые 2 бизнес-правила, а вот за валидацию количества (и, возможно, других своих свойств) может (и должен) отвечать OrderItem. Можно, конечно, пойти дальше и использовать для свойства Quantity тип, не допускающий отрицательных значений, но сегодня не об этом.Order, отвечающий за валидность всего агрегата, может при этом использовать поведение OrderItem. Ему не нужно проверять OrderItem извне, если он может быть уверен, что дочерний объект валиден. Вам же не нужно проверять, что свойство Month типа DateTime меньше 13. Тип делает эту проверку за вас, поэтому вы можете быть уверены, что месяц будет в допустимом диапазоне, если у вас есть экземпляр типа.Многие разработчики также считают, что правило агрегата, гарантирующее, что весь агрегат является «действительным», требует метода для проверки достоверности. Иногда это полезно, но лучший дизайн — это тот, который изначально не допускает существования недопустимых значений. Снова рассмотрим тип
DateTime. У него нет метода IsValid(), который вы вызываете после создания экземпляра, указав год, месяц и день как 2022, 50 и 50. Тип просто не позволит вам установить свои месяц или день в значение 50. Если вам удастся создать экземпляр DateTime, вы можете быть уверены, что он валиден, а также вы не сможете использовать ни один из его методов, чтобы поместить его в недопустимое состояние. Этот же подход нужно использовать со своими агрегатами и сущностями.Итого
Корень агрегата отвечает за то, чтобы агрегат находился в допустимом состоянии. Но это не означает, что каждая операция, выполняемая над любым элементом агрегата, должна находиться в корне агрегата. Это также не означает, что агрегат должен иметь метод, выполняющий проверку. Использование надлежащего объектно-ориентированного дизайна и обеспечение того, чтобы ваши объекты инкапсулировали и управляли своим собственным состоянием, обеспечивает более чистый дизайн.
См. также Сущности и Объекты-Значения
Источник: https://ardalis.com/aggregate-responsibility-design/
👍11
День 1332. #ЗаметкиНаПолях #DDD
Всегда Валидная Модель Домена
Инвариант — это условие, которое всегда должно выполняться. Например, треугольник имеет 3 стороны. Условие
Другое важное свойство инвариантов состоит в том, что они определяют класс предметной области: благодаря им этот класс является тем, чем он является. Следовательно, вы не можете нарушить эти инварианты. Если вы это сделаете, доменный класс просто перестанет быть тем, что вы от него ожидаете, а станет чем-то другим. Например, если вы добавите четвёртую сторону к треугольнику, он станет четырёхугольником.
Наличие инвариантов — это то, что требует введения правил валидации. Без таких инвариантов, как
Таким образом, разница между валидацией и инвариантами — это просто вопрос точки зрения. Одни и те же бизнес-правила рассматриваются как инварианты моделью предметной области и как правила валидации сервисами приложений.
Это различие приводит к различному обращению с нарушениями этих бизнес-правил. Нарушение инварианта в модели предметной области — это исключительная ситуация, и на неё следует генерировать исключение и полностью останавливать текущую операцию (принцип отказоустойчивости).
С другой стороны, нет ничего исключительного в том, что внешний ввод неверен. Для этого и нужны прикладные сервисы: они разделяют (фильтруют) правильные и неправильные запросы. Вы не должны генерировать исключения в таких случаях и вместо этого должны использовать класс Result со статусом операции.
Можно предположить, что разница между валидациями и инвариантами в том, что валидации могут меняться в зависимости от бизнес-правил. Например, треугольник имеет 2 инварианта:
- ровно 3 стороны,
- каждая сторона больше нуля.
Но в нашей модели предметной области есть ещё условие:
- каждая сторона долна быть больше 10 см.
Это правило валидации (в отличие от инвариантов) можно изменить или удалить из нашей модели предметной области.
Действительно, интуитивно эти два условия не кажутся одинаковыми: наличие 3 сторон и требование, чтобы все стороны были больше 10 см. Одно условие необходимо для треугольников, а другое явно нет. Но это только потому, что мы привносим наш реальный опыт в область моделирования предметной области.
Какова цель моделирования предметной области? Максимально приблизиться к физическому миру? Сделать модель максимально реалистичной?
Нет.
Цель в том, чтобы построить модель, полезную для нашей конкретной проблемы. Не для всех возможных областей проблем и определённо не для какой-то сферической проблемы в вакууме. Для нашей конкретной.
Следовательно, если нашему приложению необходимо, чтобы все треугольники имели стороны больше 10 см, если работа с треугольниками меньших размеров не помогает нам достичь наших целей, то эти меньшие треугольники могут просто не существовать для целей нашего приложения. Т.е., если концепция бесполезна для модели, вы вообще не должны включать её в модель.
Да, треугольники со сторонами меньше 10 см могут существовать в других доменах, но в нашем конкретном их нет. Так же, как и четырёхугольники, пятиугольники и другие фигуры (при условии, что наше приложение работает только с треугольниками). Поэтому между этими условиями наличия ровно 3 сторон и всех сторон больше 10 см нет разницы. Оба являются инвариантами, составляющими понятие треугольника в нашем конкретном приложении.
Конечно, требования могут измениться, и условие 10 см может превратиться в 5 или 20 (или даже стать настраиваемым), но это регулярный процесс уточнения, когда вы лучше понимаете домен по мере продвижения проекта. Это не означает, что исходное условие не было инвариантом. Было. Так же, как новое является инвариантом сейчас.
Источник: https://khorikov.org/posts/2022-06-06-validation-vs-invariants/
Всегда Валидная Модель Домена
Инвариант — это условие, которое всегда должно выполняться. Например, треугольник имеет 3 стороны. Условие
edges.Count == 3 по своей сути верно для всех треугольников.Другое важное свойство инвариантов состоит в том, что они определяют класс предметной области: благодаря им этот класс является тем, чем он является. Следовательно, вы не можете нарушить эти инварианты. Если вы это сделаете, доменный класс просто перестанет быть тем, что вы от него ожидаете, а станет чем-то другим. Например, если вы добавите четвёртую сторону к треугольнику, он станет четырёхугольником.
Наличие инвариантов — это то, что требует введения правил валидации. Без таких инвариантов, как
edges.Count == 3, вам не нужно было бы проверять входные данные от внешних клиентов.Таким образом, разница между валидацией и инвариантами — это просто вопрос точки зрения. Одни и те же бизнес-правила рассматриваются как инварианты моделью предметной области и как правила валидации сервисами приложений.
Это различие приводит к различному обращению с нарушениями этих бизнес-правил. Нарушение инварианта в модели предметной области — это исключительная ситуация, и на неё следует генерировать исключение и полностью останавливать текущую операцию (принцип отказоустойчивости).
С другой стороны, нет ничего исключительного в том, что внешний ввод неверен. Для этого и нужны прикладные сервисы: они разделяют (фильтруют) правильные и неправильные запросы. Вы не должны генерировать исключения в таких случаях и вместо этого должны использовать класс Result со статусом операции.
Можно предположить, что разница между валидациями и инвариантами в том, что валидации могут меняться в зависимости от бизнес-правил. Например, треугольник имеет 2 инварианта:
- ровно 3 стороны,
- каждая сторона больше нуля.
Но в нашей модели предметной области есть ещё условие:
- каждая сторона долна быть больше 10 см.
Это правило валидации (в отличие от инвариантов) можно изменить или удалить из нашей модели предметной области.
Действительно, интуитивно эти два условия не кажутся одинаковыми: наличие 3 сторон и требование, чтобы все стороны были больше 10 см. Одно условие необходимо для треугольников, а другое явно нет. Но это только потому, что мы привносим наш реальный опыт в область моделирования предметной области.
Какова цель моделирования предметной области? Максимально приблизиться к физическому миру? Сделать модель максимально реалистичной?
Нет.
Цель в том, чтобы построить модель, полезную для нашей конкретной проблемы. Не для всех возможных областей проблем и определённо не для какой-то сферической проблемы в вакууме. Для нашей конкретной.
Следовательно, если нашему приложению необходимо, чтобы все треугольники имели стороны больше 10 см, если работа с треугольниками меньших размеров не помогает нам достичь наших целей, то эти меньшие треугольники могут просто не существовать для целей нашего приложения. Т.е., если концепция бесполезна для модели, вы вообще не должны включать её в модель.
Да, треугольники со сторонами меньше 10 см могут существовать в других доменах, но в нашем конкретном их нет. Так же, как и четырёхугольники, пятиугольники и другие фигуры (при условии, что наше приложение работает только с треугольниками). Поэтому между этими условиями наличия ровно 3 сторон и всех сторон больше 10 см нет разницы. Оба являются инвариантами, составляющими понятие треугольника в нашем конкретном приложении.
Конечно, требования могут измениться, и условие 10 см может превратиться в 5 или 20 (или даже стать настраиваемым), но это регулярный процесс уточнения, когда вы лучше понимаете домен по мере продвижения проекта. Это не означает, что исходное условие не было инвариантом. Было. Так же, как новое является инвариантом сейчас.
Источник: https://khorikov.org/posts/2022-06-06-validation-vs-invariants/
👍9
День 1357. #ЗаметкиНаПолях #DDD
Маппинг Ошибок. Чья Это Ответственность?
Допустим, модель предметной области возвращает ошибку, о которой нельзя сообщать пользователю, поэтому её нужно смаппить в дружелюбное пользователю сообщение или просто очистить от конфиденциальных данных. Должно ли это происходить на уровне домена или в контроллере (на прикладном уровне)?
Кажется, что «чище» поместить этот код в контроллер, так как это зона ответственности контроллера. С другой стороны, это похоже на проблему домена.
Это довольно типичный случай. Самый распространенный пример: приложение проверяет имя пользователя и пароль при входе в систему. В этом сценарии может быть несколько ошибок проверки, в том числе:
- пользователь не найден в базе;
- имя пользователя не соответствует основным правилам проверки (например, оно слишком короткое);
- пароль неверный и т.д.
Из соображений безопасности вы не можете отобразить пользователю точную ошибку проверки. Все, что вы можете показать, это что-то обобщённое: «Имя пользователя или пароль неверны». Где должно происходить сопоставление конкретной ошибки с обобщённым сообщением? На уровне домена или приложения?
Это должно происходить на уровне домена.
Маппинг/очистка сообщения об ошибке является проблемой предметной области. Это исходит из бизнес-требований, что означает, что должно быть частью домена. Введите отдельный класс предметной области, такой как
Это не только поставит логику на место, но также упростит контроллер и улучшит тестируемость: намного проще создать модульный тест метода, возвращающего ошибку и маппинга ошибок, чем тестировать целый контроллер, работающий с несколькими внепроцессными зависимостями.
Это, кстати, суть паттерна Humble Object. Идея состоит в том, чтобы извлечь важную часть логики (маппер ошибок) из контроллеров, чтобы упростить модульное тестирование этой логики.
Источник: https://khorikov.org/posts/2022-02-28-error-mapping/
Маппинг Ошибок. Чья Это Ответственность?
Допустим, модель предметной области возвращает ошибку, о которой нельзя сообщать пользователю, поэтому её нужно смаппить в дружелюбное пользователю сообщение или просто очистить от конфиденциальных данных. Должно ли это происходить на уровне домена или в контроллере (на прикладном уровне)?
Кажется, что «чище» поместить этот код в контроллер, так как это зона ответственности контроллера. С другой стороны, это похоже на проблему домена.
Это довольно типичный случай. Самый распространенный пример: приложение проверяет имя пользователя и пароль при входе в систему. В этом сценарии может быть несколько ошибок проверки, в том числе:
- пользователь не найден в базе;
- имя пользователя не соответствует основным правилам проверки (например, оно слишком короткое);
- пароль неверный и т.д.
Из соображений безопасности вы не можете отобразить пользователю точную ошибку проверки. Все, что вы можете показать, это что-то обобщённое: «Имя пользователя или пароль неверны». Где должно происходить сопоставление конкретной ошибки с обобщённым сообщением? На уровне домена или приложения?
Это должно происходить на уровне домена.
Маппинг/очистка сообщения об ошибке является проблемой предметной области. Это исходит из бизнес-требований, что означает, что должно быть частью домена. Введите отдельный класс предметной области, такой как
ErrorMapper, ErrorCleaner или что-то подобное.Это не только поставит логику на место, но также упростит контроллер и улучшит тестируемость: намного проще создать модульный тест метода, возвращающего ошибку и маппинга ошибок, чем тестировать целый контроллер, работающий с несколькими внепроцессными зависимостями.
Это, кстати, суть паттерна Humble Object. Идея состоит в том, чтобы извлечь важную часть логики (маппер ошибок) из контроллеров, чтобы упростить модульное тестирование этой логики.
Источник: https://khorikov.org/posts/2022-02-28-error-mapping/
👍20
День 1636. #DDD
Использование Доменных Событий для Построения Слабосвязанных Систем. Начало
В программной инженерии «связанность» означает, насколько разные части программной системы зависят друг от друга. Если они тесно связаны, изменения в одной части могут повлиять на другие. Если слабо, изменения в одной части не вызовут больших проблем в остальной системе. Доменные события — это тактический шаблон DDD, который можно использовать для создания слабосвязанных систем.
Событие представляет собой произошедший в домене факт. Другие компоненты в системе могут подписаться на это событие и соответствующим образом его обработать. События домена позволяют явно выражать побочные эффекты и обеспечивают лучшее разделение проблем в домене. Это идеальный способ вызвать побочные эффекты для нескольких агрегатов внутри домена.
Вы несёте ответственность за то, чтобы публикация события предметной области была транзакционной (об этом позже).
Особенности
- Неизменяемость — доменные события являются фактами и должны быть неизменными.
- Толстые и Тонкие события — сколько информации вам нужно передавать?
- Используйте прошедшее время для именования событий.
Доменные события и интеграционные события
Семантически это одно и то же: представление чего-то, что произошло в прошлом. Однако намерения у них разные.
События домена:
- публикуются и используется в пределах одного домена;
- отправляются с помощью шины сообщений в памяти;
- могут обрабатываться синхронно или асинхронно.
Интеграционные события:
- потребляются другими подсистемами (микросервисы, ограниченные контексты);
- отправляются брокером сообщений через очередь;
- обрабатываются только асинхронно.
Доменные события можно использовать для создания интеграционных событий, выходящих за пределы домена.
Реализация
Реализовать доменные события можно через создание абстракции IDomainEvent и реализацию INotification в MediatR. Так вы можете использовать поддержку публикации-подписки в MediatR для публикации уведомления одному или нескольким обработчикам.
Только сущности могут вызывать доменные события, поэтому создадим базовый класс Entity, сделав метод RaiseDomainEvent защищённым. Доменные события хранятся во внутренней коллекции, чтобы никто не мог получить к ним доступ. GetDomainEvents предназначен для получения моментального снимка коллекции, ClearDomainEvents — для очистки.
Источник: https://www.milanjovanovic.tech/blog/using-domain-events-to-build-loosely-coupled-systems
Использование Доменных Событий для Построения Слабосвязанных Систем. Начало
В программной инженерии «связанность» означает, насколько разные части программной системы зависят друг от друга. Если они тесно связаны, изменения в одной части могут повлиять на другие. Если слабо, изменения в одной части не вызовут больших проблем в остальной системе. Доменные события — это тактический шаблон DDD, который можно использовать для создания слабосвязанных систем.
Событие представляет собой произошедший в домене факт. Другие компоненты в системе могут подписаться на это событие и соответствующим образом его обработать. События домена позволяют явно выражать побочные эффекты и обеспечивают лучшее разделение проблем в домене. Это идеальный способ вызвать побочные эффекты для нескольких агрегатов внутри домена.
Вы несёте ответственность за то, чтобы публикация события предметной области была транзакционной (об этом позже).
Особенности
- Неизменяемость — доменные события являются фактами и должны быть неизменными.
- Толстые и Тонкие события — сколько информации вам нужно передавать?
- Используйте прошедшее время для именования событий.
Доменные события и интеграционные события
Семантически это одно и то же: представление чего-то, что произошло в прошлом. Однако намерения у них разные.
События домена:
- публикуются и используется в пределах одного домена;
- отправляются с помощью шины сообщений в памяти;
- могут обрабатываться синхронно или асинхронно.
Интеграционные события:
- потребляются другими подсистемами (микросервисы, ограниченные контексты);
- отправляются брокером сообщений через очередь;
- обрабатываются только асинхронно.
Доменные события можно использовать для создания интеграционных событий, выходящих за пределы домена.
Реализация
Реализовать доменные события можно через создание абстракции IDomainEvent и реализацию INotification в MediatR. Так вы можете использовать поддержку публикации-подписки в MediatR для публикации уведомления одному или нескольким обработчикам.
using MediatR;Вызов
public interface IDomainEvent : INotification
{
}
public class CourseCompleted : IDomainEvent
{
public Guid CourseId { get; init; }
}
Только сущности могут вызывать доменные события, поэтому создадим базовый класс Entity, сделав метод RaiseDomainEvent защищённым. Доменные события хранятся во внутренней коллекции, чтобы никто не мог получить к ним доступ. GetDomainEvents предназначен для получения моментального снимка коллекции, ClearDomainEvents — для очистки.
public abstract class Entity : IEntityТеперь сущности могут наследовать от Entity и вызывать доменные события:
{
private readonly List<IDomainEvent>
_events = new();
public IReadOnlyList<IDomainEvent>
GetDomainEvents() => _events.ToList();
public void ClearDomainEvents()
=> _events.Clear();
protected void RaiseDomainEvent(
IDomainEvent domainEvent)
=> _events.Add(domainEvent);
}
public class Course : EntityОкончание следует…
{
public Guid Id { get; private set; }
public CourseStatus Status { get; private set; }
public DateTime? Completed { get; private set; }
public void Complete()
{
Status = CourseStatus.Completed;
Completed = DateTime.UtcNow;
RaiseDomainEvent(
new CourseCompleted {
CourseId = this.Id
});
}
}
Источник: https://www.milanjovanovic.tech/blog/using-domain-events-to-build-loosely-coupled-systems
👍16
День 1637. #DDD
Использование Доменных Событий для Построения Слабосвязанных Систем. Окончание
Начало
Публикация с помощью EF Core
Поскольку EF Core использует паттерн Единица Работы, вы можете использовать его для сбора всех доменных событий в текущей транзакции и их публикации.
Можно либо переопределить метод SaveChangesAsync, либо использовать перехватчик:
До:
- события являются частью той же транзакции,
- немедленная согласованность данных.
После:
- события – отдельная транзакция,
- конечная согласованность, т.к. сообщения обрабатываются после исходной транзакции,
- риск несогласованности БД, т.к. обработка события может привести к сбою.
Можно решить эту проблему с помощью паттерна исходящих сообщений (Outbox), когда изменения в БД и изменения, сделанные в доменном событии сохраняются (в виде исходящих сообщений) в одной транзакции.
Метода PublishDomainEventsAsync:
Нужно определить класс, реализующий INotificationHandler<T>, где T – тип доменного события. Обработчик доменного события CourseCompleted ниже публикует CourseCompletedIntegrationEvent для уведомления других систем.
- Доменные события могут помочь построить слабосвязанную систему, отделяя основную логику домена от побочных эффектов, которые можно обрабатывать асинхронно.
- Для реализации доменных событий, можно использовать библиотеки EF Core и MediatR.
- Нужно решить, когда публиковать доменные события: до или после сохранения изменений в БД.
- Публикация доменных событий после сохранения изменений в БД и паттерн Outbox для добавления транзакционных гарантий обеспечивают окончательную согласованность, но также бОльшую надёжность.
Источник: https://www.milanjovanovic.tech/blog/using-domain-events-to-build-loosely-coupled-systems
Использование Доменных Событий для Построения Слабосвязанных Систем. Окончание
Начало
Публикация с помощью EF Core
Поскольку EF Core использует паттерн Единица Работы, вы можете использовать его для сбора всех доменных событий в текущей транзакции и их публикации.
Можно либо переопределить метод SaveChangesAsync, либо использовать перехватчик:
public class ApplicationDbContext : DbContextВажное решение: публиковать доменные события до или после вызова SaveChangesAsync (сохранения данных в БД)?
{
public override async Task<int> SaveChangesAsync(
CancellationToken ct = default)
{
var result = await
base.SaveChangesAsync(ct);
await PublishDomainEventsAsync();
return result;
}
}
До:
- события являются частью той же транзакции,
- немедленная согласованность данных.
После:
- события – отдельная транзакция,
- конечная согласованность, т.к. сообщения обрабатываются после исходной транзакции,
- риск несогласованности БД, т.к. обработка события может привести к сбою.
Можно решить эту проблему с помощью паттерна исходящих сообщений (Outbox), когда изменения в БД и изменения, сделанные в доменном событии сохраняются (в виде исходящих сообщений) в одной транзакции.
Метода PublishDomainEventsAsync:
private async Task PublishDomainEventsAsync()Обработка
{
var events = ChangeTracker
.Entries<Entity>()
.Select(e => e.Entity)
.SelectMany(ent =>
{
var evnts = ent.GetDomainEvents();
ent.ClearDomainEvents();
return evnts;
})
.ToList();
foreach (var ev in events)
await _mediator.Publish(ev);
}
Нужно определить класс, реализующий INotificationHandler<T>, где T – тип доменного события. Обработчик доменного события CourseCompleted ниже публикует CourseCompletedIntegrationEvent для уведомления других систем.
public class CourseCompletedDomainEventHandlerИтого
: INotificationHandler<CourseCompleted>
{
private readonly IBus _bus;
public CourseCompletedDomainEventHandler(IBus bus)
{
_bus = bus;
}
public async Task Handle(
CourseCompleted de,
CancellationToken ct)
{
await _bus.Publish(
new CourseCompletedIntegrationEvent(de.CourseId),
ct);
}
}
- Доменные события могут помочь построить слабосвязанную систему, отделяя основную логику домена от побочных эффектов, которые можно обрабатывать асинхронно.
- Для реализации доменных событий, можно использовать библиотеки EF Core и MediatR.
- Нужно решить, когда публиковать доменные события: до или после сохранения изменений в БД.
- Публикация доменных событий после сохранения изменений в БД и паттерн Outbox для добавления транзакционных гарантий обеспечивают окончательную согласованность, но также бОльшую надёжность.
Источник: https://www.milanjovanovic.tech/blog/using-domain-events-to-build-loosely-coupled-systems
👍12
День 1791. #DDD
Объекты-Значения в .NET. Начало
Объектом-значением (value object) называется объект, который представляет описательный аспект предметной области и не имеет собственной идентичности. Такие объекты создаются в программе для представления элементов проекта, о которых достаточно знать только, что они собой представляют, но не каким именно предметом они являются.
— Эрик Эванс
Объекты-значения инкапсулируют примитивные типы в предметной области и решают проблему одержимости примитивами.
Основные качества:
- неизменяемость,
- отсутствие идентичности,
- структурное равенство (два объекта-значения равны, если их значения одинаковы).
Реализация
Объект Booking ниже с примитивными значениями, представляющими адрес, а также даты начала и окончания бронирования:
Можно заменить эти примитивы объектами-значениями Address и DateRange:
1. Записи
В С# для представления объектов-значений можно использовать записи. Записи неизменяемы, реализуют структурное равенство и лаконичны за счёт использования первичных конструкторов:
Однако, при этом вы теряете возможность реализовывать инварианты (например, валидацию параметров). Также проблема инвариантов объектов-значений возникает при использовании выражения with.
2. Базовый класс
Альтернативный способ — использование базового класса ValueObject с переопределением равенства (метод GetValues). Наследники должны реализовать этот метод и определить компоненты равенства (равенство каких свойств означает равенство объектов).
Преимущество ValueObject в его явности. Понятно, какие классы домена представляют объекты-значения и какие свойства отвечают за равенство.
Реализация объекта-значения Address:
Окончание следует…
Источник: https://www.milanjovanovic.tech/blog/value-objects-in-dotnet-ddd-fundamentals
Объекты-Значения в .NET. Начало
Объектом-значением (value object) называется объект, который представляет описательный аспект предметной области и не имеет собственной идентичности. Такие объекты создаются в программе для представления элементов проекта, о которых достаточно знать только, что они собой представляют, но не каким именно предметом они являются.
— Эрик Эванс
Объекты-значения инкапсулируют примитивные типы в предметной области и решают проблему одержимости примитивами.
Основные качества:
- неизменяемость,
- отсутствие идентичности,
- структурное равенство (два объекта-значения равны, если их значения одинаковы).
Реализация
Объект Booking ниже с примитивными значениями, представляющими адрес, а также даты начала и окончания бронирования:
public class Booking
{
public string Street { get; init; }
public string City { get; init; }
public string Country { get; init; }
public DateOnly StartDate { get; init; }
public DateOnly EndDate { get; init; }
}
Можно заменить эти примитивы объектами-значениями Address и DateRange:
public class Booking
{
public Address Address { get; init; }
public DateRange Period { get; init; }
}
1. Записи
В С# для представления объектов-значений можно использовать записи. Записи неизменяемы, реализуют структурное равенство и лаконичны за счёт использования первичных конструкторов:
public record Address(
string Street,
string City,
string Country
);
Однако, при этом вы теряете возможность реализовывать инварианты (например, валидацию параметров). Также проблема инвариантов объектов-значений возникает при использовании выражения with.
2. Базовый класс
Альтернативный способ — использование базового класса ValueObject с переопределением равенства (метод GetValues). Наследники должны реализовать этот метод и определить компоненты равенства (равенство каких свойств означает равенство объектов).
Преимущество ValueObject в его явности. Понятно, какие классы домена представляют объекты-значения и какие свойства отвечают за равенство.
public abstract class ValueObject
: IEquatable<ValueObject>
{
public static bool operator ==
(ValueObject? a, ValueObject? b)
{
if (a is null && b is null)
return true;
if (a is null || b is null)
return false;
return a.Equals(b);
}
public static bool operator !=
(ValueObject? a, ValueObject? b)
=> !(a == b);
public virtual bool Equals
(ValueObject? other)
=> other is not null && ValuesEqual(other);
public override bool Equals
(object? obj)
=> obj is ValueObject v
&& ValuesEqual(v);
public override int GetHashCode() =>
GetValues().Aggregate(
default(int),
(hash, value) =>
HashCode.Combine(hash, value.GetHashCode()));
protected abstract IEnumerable<object> GetValues();
private bool ValuesEqual(ValueObject v) =>
GetValues().SequenceEqual(v.GetValues());
}
Реализация объекта-значения Address:
public sealed class Address : ValueObject
{
public string Street { get; init; }
public string City { get; init; }
public string Country { get; init; }
protected override IEnumerable<object> GetValues()
{
yield return Street;
yield return City;
yield return Country;
}
}
Окончание следует…
Источник: https://www.milanjovanovic.tech/blog/value-objects-in-dotnet-ddd-fundamentals
👍16
День 1792. #DDD
Объекты-Значения в .NET. Окончание
Начало
Когда использовать объекты-значения?
Для решения проблемы одержимости примитивами и инкапсуляции инвариантов домена. Инкапсуляция — важный аспект любой модели предметной области. У вас не должно быть возможности создать объект значения в недопустимом состоянии.
Объекты-значения также обеспечивают безопасность типов. Сравните:
С реализацией, в которую добавлен объекты-значения:
Второй вариант гораздо яснее выражает намерение и снижает вероятность ошибок (например, перепутанных дат).
Однако, если применить инварианты нужно только в нескольких местах кода, лучше обойтись без объектов-значений.
Сохранение объектов-значений с помощью EF Core
Объекты-значения можно сохранять, используя принадлежащие (Owned) и комплексные (Complex) типы EF Core.
Принадлежащие типы можно настроить, вызвав метод OwnsOne при настройке сущности. Это укажет EF сохранять объекты-значений в той же таблице в виде дополнительных столбцов:
Замечания:
- Принадлежащие типы имеют скрытое значение ключа.
- Нет поддержки необязательных (обнуляемых) принадлежащих типов.
- Поддерживаются принадлежащие коллекции с помощью OwnsMany.
- Разделение таблиц позволяет сохранять принадлежащие типы отдельно.
Комплексные типы
Это новая функция EF, доступная в .NET 8. Они не идентифицируются и не отслеживаются по значению ключа, поэтому лучше подходят для хранения объектов-значений. Комплексные типы должны быть частью типа сущности:
Ограничения для комплексных типов:
- Нет поддержки коллекций.
- Нет поддержки обнуляемых значений.
Итого
Объекты-значения помогают разработать богатую модель предметной области. Вы можете использовать их для решения проблемы одержимости примитивами и инкапсуляции инвариантов предметной области. Объекты-значения могут уменьшить количество ошибок, предотвращая создание недопустимых объектов домена.
Вы можете использовать записи или базовый класс ValueObject для представления объектов-значений. Это должно зависеть от конкретных требований и сложности домена.
Источник: https://www.milanjovanovic.tech/blog/value-objects-in-dotnet-ddd-fundamentals
Объекты-Значения в .NET. Окончание
Начало
Когда использовать объекты-значения?
Для решения проблемы одержимости примитивами и инкапсуляции инвариантов домена. Инкапсуляция — важный аспект любой модели предметной области. У вас не должно быть возможности создать объект значения в недопустимом состоянии.
Объекты-значения также обеспечивают безопасность типов. Сравните:
public interface IPricingService
{
decimal Calculate(
Apartment apartment,
DateOnly start,
DateOnly end);
}
С реализацией, в которую добавлен объекты-значения:
public interface IPricingService
{
PricingDetails Calculate(
Apartment apartment,
DateRange period);
}
Второй вариант гораздо яснее выражает намерение и снижает вероятность ошибок (например, перепутанных дат).
Однако, если применить инварианты нужно только в нескольких местах кода, лучше обойтись без объектов-значений.
Сохранение объектов-значений с помощью EF Core
Объекты-значения можно сохранять, используя принадлежащие (Owned) и комплексные (Complex) типы EF Core.
Принадлежащие типы можно настроить, вызвав метод OwnsOne при настройке сущности. Это укажет EF сохранять объекты-значений в той же таблице в виде дополнительных столбцов:
public void Configure(
EntityTypeBuilder<Apartment> builder)
{
builder.ToTable("apartments");
builder.OwnsOne(p => p.Address);
builder.OwnsOne(p => p.Price, b =>
{
b.Property(m => m.Currency)
.HasConversion(
curr => curr.Code,
code => Currency.FromCode(code));
});
}
Замечания:
- Принадлежащие типы имеют скрытое значение ключа.
- Нет поддержки необязательных (обнуляемых) принадлежащих типов.
- Поддерживаются принадлежащие коллекции с помощью OwnsMany.
- Разделение таблиц позволяет сохранять принадлежащие типы отдельно.
Комплексные типы
Это новая функция EF, доступная в .NET 8. Они не идентифицируются и не отслеживаются по значению ключа, поэтому лучше подходят для хранения объектов-значений. Комплексные типы должны быть частью типа сущности:
public void Configure(
EntityTypeBuilder<Apartment> builder)
{
builder.ToTable("apartments");
builder.ComplexProperty(p => p.Address);
}
Ограничения для комплексных типов:
- Нет поддержки коллекций.
- Нет поддержки обнуляемых значений.
Итого
Объекты-значения помогают разработать богатую модель предметной области. Вы можете использовать их для решения проблемы одержимости примитивами и инкапсуляции инвариантов предметной области. Объекты-значения могут уменьшить количество ошибок, предотвращая создание недопустимых объектов домена.
Вы можете использовать записи или базовый класс ValueObject для представления объектов-значений. Это должно зависеть от конкретных требований и сложности домена.
Источник: https://www.milanjovanovic.tech/blog/value-objects-in-dotnet-ddd-fundamentals
👍9