Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Подсказка
Это фрагмент из электронной книги «Архитектура микрослужб .NET для контейнеризованных приложений .NET», доступной в документации .NET или в виде бесплатного скачиваемого PDF-файла, который можно прочитать в автономном режиме.
В предыдущем разделе были описаны основные принципы и шаблоны проектирования модели предметной области. Теперь пришло время изучить возможные способы реализации модели домена с помощью .NET (обычного кода C#) и EF Core. Модель домена будет состоять только из вашего кода. Он будет иметь только требования к модели EF Core, но не реальные зависимости от EF. У вас не должно быть жестких зависимостей или ссылок на EF Core или любой другой ORM в модели домена.
Структура модели домена в пользовательской библиотеке .NET Standard
Организация папок, используемая для эталонного приложения eShopOnContainers, демонстрирует модель DDD для приложения. Возможно, вы обнаружите, что другая организация папок более четко передает дизайнерские решения, принятые для вашего приложения. Как видно на рис. 7-10, в модели упорядочения домена есть два агрегата, агрегат заказа и агрегат покупателя. Каждый агрегат — это группа доменных сущностей и объектов значений, хотя вы также можете иметь агрегат, состоящий из одной сущности домена (корневой сущности агрегата).
Представление обозревателя решений для проекта Ordering.Domain с папкой AggregatesModel, содержащей папки BuyerAggregate и OrderAggregate, каждый из которых содержит классы сущностей, файлы объектов значения и т. д.
Рис. 7-10. Структура модели домена для микрослужбы заказа в eShopOnContainers
Кроме того, уровень модели домена включает контракты репозитория (интерфейсы), которые являются требованиями к инфраструктуре модели домена. Другими словами, эти интерфейсы выражают, какие репозитории и методы должны реализовывать слой инфраструктуры. Важно, чтобы реализация репозиториев была помещена за пределы уровня модели домена в библиотеке слоев инфраструктуры, поэтому уровень модели домена не "загрязнен" API или классами из технологий инфраструктуры, таких как Entity Framework.
Вы также можете увидеть папку SeedWork , содержащую пользовательские базовые классы, которые можно использовать в качестве базы для сущностей домена и объектов значений, поэтому у вас нет избыточного кода в классе объектов каждого домена.
Агрегаты структуры в пользовательской библиотеке .NET Standard
Агрегат ссылается на кластер объектов домена, сгруппированных вместе, чтобы соответствовать согласованности транзакций. Эти объекты могут быть экземплярами сущностей (одним из которых является агрегатная корневая или корневая сущность) и любыми дополнительными объектами значений.
Согласованность транзакций означает, что агрегат гарантированно будет согласованным и актуальным в конце бизнес-действия. Например, агрегат заказа в модели домена микросервиса eShopOnContainers для заказов составлен, как показано на рис. 7-11.
Подробное представление папки OrderAggregate: Address.cs является объектом значения, IOrderRepository — это интерфейс репозитория, Order.cs является корнем агрегата, OrderItem.cs является дочерней сущностью, и OrderStatus.cs является классом перечисления.
Рис. 7–11. Агрегация заказов в решении Visual Studio
Если открыть любой из файлов в агрегированной папке, вы увидите, как он помечен как пользовательский базовый класс или интерфейс, например, сущность или объект значения, как реализовано в папке SeedWork.
Реализация сущностей домена в виде классов POCO
Вы реализуете модель домена в .NET, создавая классы POCO, реализующие сущности домена. В следующем примере класс Order определяется как сущность, а также как корень агрегата. Так как класс Order является производным от базового класса Entity, он может повторно использовать общий код, связанный с сущностями. Помните, что эти базовые классы и интерфейсы определяются вами в проекте модели домена, поэтому это код, а не код инфраструктуры из ORM, например EF.
// COMPATIBLE WITH ENTITY FRAMEWORK CORE 5.0
// Entity is a custom base class with the ID
public class Order : Entity, IAggregateRoot
{
private DateTime _orderDate;
public Address Address { get; private set; }
private int? _buyerId;
public OrderStatus OrderStatus { get; private set; }
private int _orderStatusId;
private string _description;
private int? _paymentMethodId;
private readonly List<OrderItem> _orderItems;
public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;
public Order(string userId, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber,
string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null)
{
_orderItems = new List<OrderItem>();
_buyerId = buyerId;
_paymentMethodId = paymentMethodId;
_orderStatusId = OrderStatus.Submitted.Id;
_orderDate = DateTime.UtcNow;
Address = address;
// ...Additional code ...
}
public void AddOrderItem(int productId, string productName,
decimal unitPrice, decimal discount,
string pictureUrl, int units = 1)
{
//...
// Domain rules/logic for adding the OrderItem to the order
// ...
var orderItem = new OrderItem(productId, productName, unitPrice, discount, pictureUrl, units);
_orderItems.Add(orderItem);
}
// ...
// Additional methods with domain rules/logic related to the Order aggregate
// ...
}
Важно отметить, что это сущность домена, реализованная как класс POCO. Она не имеет прямой зависимости от Entity Framework Core или любой другой инфраструктуры. Эта реализация соответствует правилам DDD, это просто C# код, который реализует модель домена.
Кроме того, класс украшен интерфейсом iAggregateRoot. Этот интерфейс является пустым интерфейсом, иногда называемым интерфейсом маркера, который используется только для указания того, что этот класс сущности также является агрегатным корнем.
Интерфейс маркера иногда считается антипаттерном; однако, это также чёткий способ пометить класс, особенно когда этот интерфейс может развиваться. Атрибут мог бы быть другим вариантом для маркера, но удобнее видеть базовый класс (Entity) рядом с интерфейсом IAggregate, чем размещать атрибут Aggregate над классом. Это вопрос предпочтений, в любом случае.
Наличие корня агрегата означает, что большая часть кода, связанного с согласованностью и бизнес-правилами сущностей агрегата, должна быть реализована как методы в корневом классе Order (например, AddOrderItem при добавлении объекта OrderItem в агрегат). Не следует создавать или обновлять объекты OrderItems независимо или напрямую; Класс AggregateRoot должен поддерживать контроль и согласованность любой операции обновления для дочерних сущностей.
Инкапсулировать данные в объектах домена
Распространенная проблема в моделях сущностей заключается в том, что они предоставляют свойства навигации коллекции как общедоступные типы списков. Это позволяет любому разработчику совместной работы управлять содержимым этих типов коллекций, что может обойти важные бизнес-правила, связанные с коллекцией, возможно, оставив объект в недопустимом состоянии. Решением этого является предоставление доступа только для чтения к связанным коллекциям и явное предоставление методов, определяющих способы управления ими клиентов.
В предыдущем коде обратите внимание, что многие атрибуты доступны только для чтения или закрыты и обновляются только методами класса, поэтому любое обновление учитывает инварианты и логику бизнес-домена, указанные в методах класса.
Например, следуя шаблонам DDD, вы не должны выполнять следующие действия из любого метода обработчика команд или класса уровня приложений (на самом деле, это должно быть невозможно для вас):
// WRONG ACCORDING TO DDD PATTERNS – CODE AT THE APPLICATION LAYER OR
// COMMAND HANDLERS
// Code in command handler methods or Web API controllers
//... (WRONG) Some code with business logic out of the domain classes ...
OrderItem myNewOrderItem = new OrderItem(orderId, productId, productName,
pictureUrl, unitPrice, discount, units);
//... (WRONG) Accessing the OrderItems collection directly from the application layer // or command handlers
myOrder.OrderItems.Add(myNewOrderItem);
//...
В этом случае метод Add является исключительно операцией добавления данных с прямым доступом к коллекции OrderItems. Поэтому большая часть логики домена, правил или проверок, связанных с этой операцией с дочерними сущностями, будет распространяться на уровне приложения (обработчики команд и контроллеры веб-API).
Если обойти корень агрегата, он не сможет гарантировать инварианты, допустимость или согласованность. В конечном итоге у вас будет код spaghetti или код скрипта транзакций.
Чтобы следовать шаблонам DDD, сущности не должны иметь публичные сеттеры в любом свойстве сущности. Изменения в сущности должны управляться явными методами с явным вездесущим языком об изменениях, которые происходят в сущности.
Кроме того, коллекции в сущности (например, элементы заказа) должны иметь свойства только для чтения (метод AsReadOnly, описанный далее). Его можно обновлять только из методов агрегатного корневого класса или методов дочерних сущностей.
Как видно в коде для корня агрегата Order, все сеттеры должны быть частными или, как минимум, только для чтения извне, чтобы любая операция с данными сущности или ее дочерними сущностями выполнялась через методы класса сущности. Это обеспечивает согласованность в управляемом и объектно-ориентированном способе вместо реализации кода скрипта транзакций.
В следующем фрагменте кода показан правильный способ кода задачи добавления объекта OrderItem в агрегат Order.
// RIGHT ACCORDING TO DDD--CODE AT THE APPLICATION LAYER OR COMMAND HANDLERS
// The code in command handlers or WebAPI controllers, related only to application stuff
// There is NO code here related to OrderItem object's business logic
myOrder.AddOrderItem(productId, productName, pictureUrl, unitPrice, discount, units);
// The code related to OrderItem params validations or domain rules should
// be WITHIN the AddOrderItem method.
//...
В этом фрагменте большинство проверок или логики, связанных с созданием объекта OrderItem, будут находиться под контролем корня агрегата Order (в методе AddOrderItem), особенно проверки и логики, связанные с другими элементами в агрегате. Например, вы можете получить тот же элемент продукта в результате нескольких вызовов AddOrderItem. В этом методе можно проанализировать элементы продукта и объединить те же элементы продукта в один объект OrderItem с несколькими единицами. Кроме того, если есть разные суммы скидки, но идентификатор продукта совпадает, скорее всего, примените более высокую скидку. Этот принцип применяется к любой другой логике домена для объекта OrderItem.
Кроме того, новая операция OrderItem(params) также будет управляться и выполняться методом AddOrderItem, который является частью корневого элемента агрегата Order. Таким образом, большая часть логики или проверок, связанных с этой операцией (особенно все, что влияет на согласованность между другими дочерними сущностями), будет находиться в одном месте, в корне агрегата. Это конечная цель шаблона агрегатного корня.
При использовании Entity Framework Core 1.1 или более поздней версии сущность DDDD можно лучше выразить, так как она позволяет сопоставлять поля в дополнение к свойствам. Это полезно при защите коллекций дочерних сущностей или объектов значений. С помощью этого улучшения можно использовать простые частные поля вместо свойств и реализовать любое обновление коллекции полей в общедоступных методах и предоставить доступ только для чтения с помощью метода AsReadOnly.
В DDD сущность должна обновляться только с помощью методов сущности или конструктора, чтобы управлять любыми инвариантами и согласованностью данных, поэтому свойства должны иметь только аксессор ‘get’. Свойства поддерживаются частными полями. К частным членам можно получить доступ только из класса. Однако существует одно исключение: EF Core также необходимо задать эти поля (поэтому он может возвращать объект с соответствующими значениями).
Сопоставление свойств только с помощью методов доступа get к полям в таблице базы данных
Сопоставление свойств с столбцами таблицы базы данных не является обязанностью домена, а частью инфраструктуры и уровня сохраняемости. Мы упоминаем здесь только так, что вы знаете о новых возможностях в EF Core 1.1 или более поздней версии, связанных с тем, как можно моделировать сущности. Дополнительные сведения об этом разделе описаны в разделе инфраструктуры и сохраняемости.
При использовании EF Core 1.0 или более поздней версии в DbContext необходимо сопоставить свойства, определенные только с помощью методов получения, с фактическими полями в таблице базы данных. Это делается с помощью метода HasField класса PropertyBuilder.
Сопоставление полей без свойств
Функция сопоставления столбцов с полями в EF Core 1.1 и более поздних версиях позволяет также не использовать свойства. Вместо этого можно просто сопоставить столбцы из таблицы с полями. Распространенный вариант использования для этого — частные поля внутреннего состояния, к которым не требуется обращаться из-за пределов сущности.
Например, в приведённом выше примере кода OrderAggregate есть несколько закрытых полей, таких как поле _paymentMethodId
, которые не имеют соответствующего свойства для установки или получения. Это поле также можно вычислить в бизнес-логике заказа и использовать из методов заказа, но его также необходимо сохранить в базе данных. Таким образом, в EF Core (начиная с версии 1.1) существует способ сопоставления поля без связанного свойства со столбцом в базе данных. Это также объясняется в разделе уровня инфраструктуры этого руководства.
Дополнительные ресурсы
Вон Вернон (Vaughn Vernon). Моделирование агрегатов, используя DDD и Entity Framework. Обратите внимание, что это не Entity Framework Core.
https://kalele.io/blog-posts/modeling-aggregates-with-ddd-and-entity-framework/Джули Лерман. Точки данных. Написание кода для разработки Domain-Driven: советы по Data-Focused devs
https://learn.microsoft.com/archive/msdn-magazine/2013/august/data-points-coding-for-domain-driven-design-tips-for-data-focused-devsУди Дахан. Как создать полностью инкапсулированные доменные модели
https://udidahan.com/2008/02/29/how-to-create-fully-encapsulated-domain-models/Стив Смит. Какова разница между DTO и POCO? \ https://ardalis.com/dto-or-poco/