Реализация модели предметной области микрослужбы с помощью .NET

Совет

Это содержимое является фрагментом из электронной книги, архитектуры микрослужб .NET для контейнерных приложений .NET, доступных в документации .NET или в виде бесплатного скачиваемого PDF-файла, который можно читать в автономном режиме.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

В предыдущем разделе были представлены основные принципы и шаблоны для проектирования модели предметной области. Теперь пришло время изучить возможные способы реализации модели домена с помощью .NET (обычного кода C#) и EF Core. Ваша модель предметной области будет состоять только из вашего кода. В ней будут реализованы требования модели EF Core, но не реальные зависимости от EF. У вас не должно быть жестких зависимостей или ссылок на EF Core или любой другой ORM в модели домена.

Структура модели предметной области в пользовательской библиотеке .NET Standard

Структура папок в образце приложения eShopOnContainers демонстрирует модель DDD для приложения. В вашем случае требованиям проекта более точно может отвечать другая структура папок. Как видно на рис. 7-10, в модели предметной области размещения заказов имеются два агрегата: агрегат заказа и агрегат покупателя. Каждый агрегат представляет собой группу сущностей предметной области и объектов значений, хотя агрегат может состоять и из одной сущности предметной области (корневой сущности агрегата).

Screenshot of the Ordering.Domain project in Solution Explorer.

Представление обозревателя решений для проекта Ordering.Domain, в котором отображается папка AggregatesModel, содержащая папки BuyerAggregate и OrderAggregate, каждая из которых содержит свои классы сущностей, объектные файлы значений и т. д.

Рис. 7-10. Структура модели предметной области для микрослужбы размещения заказов в eShopOnContainers

Кроме того, уровень модели предметной области включает в себя контракты репозиториев (интерфейсы), которые представляют требования к инфраструктуре, предъявляемые моделью предметной области. Иными словами, эти интерфейсы описывают то, какие репозитории и методы должен реализовывать уровень инфраструктуры. Важно, чтобы реализация репозиториев была помещена за пределы уровня модели домена в библиотеке слоев инфраструктуры, поэтому уровень модели домена не "загрязнен" API или классами из технологий инфраструктуры, таких как Entity Framework.

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

Структура агрегатов в пользовательской библиотеке .NET Standard

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

Согласованность транзакций гарантирует согласованность и актуальность агрегата по завершении бизнес-действия. Например, агрегат заказа из модели предметной области микрослужбы размещения заказов eShopOnContainers состоит из компонентов, показанных на рис. 7-11.

Screenshot of the OrderAggregate folder and its classes.

Подробное представление папки 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 вместо того, чтобы поместить маркер атрибута Агрегата над классом. Это вопрос предпочтений, в любом случае.

Наличие корневой сущности агрегата означает, что большая часть кода, связанного с обеспечением согласованности и бизнес-правилами сущностей агрегата, должна реализовываться в виде методов класса корневой сущности агрегата 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, сущности не должны иметь открытых методов задания ни в одном свойстве сущности. Изменения в сущности должны управляться явными методами с явным ubiquitous языком об изменении, которое они выполняют в сущности.

Кроме того, коллекции в сущности (например, элементы заказа) должны быть свойствами, доступными только для чтения (см. описание метода 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.

Кроме того, операция new OrderItem(params) также будет контролироваться и выполняться методом AddOrderItem из корневой сущности агрегата Order. Поэтому большая часть логики или проверок, связанных с этой операцией (особенно все, что влияет на согласованность дочерних сущностей), будет находиться в одном месте в корневой сущности агрегата. Это конечная цель шаблона корневой сущности агрегата.

В Entity Framework Core 1.1 или более поздней версии сущность DDD можно описать лучше благодаря возможности сопоставления с полями, помимо свойств. Это полезно при защите коллекций дочерних сущностей или объектов значений. Благодаря этому улучшению вы можете использовать простые закрытые поля вместо свойств и реализовать любое изменение коллекции полей в открытых методах, предоставив доступ только для чтения посредством метода 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) существует способ сопоставления поля без связанного свойства со столбцом в базе данных. Об этом также рассказывается в разделе Уровень инфраструктуры этого руководства.

Дополнительные ресурсы