Implementowanie modelu domeny mikrousług przy użyciu platformy .NET
Napiwek
Ta zawartość jest fragmentem książki eBook, architektury mikrousług platformy .NET dla konteneryzowanych aplikacji platformy .NET dostępnych na platformie .NET Docs lub jako bezpłatnego pliku PDF, który można odczytać w trybie offline.
W poprzedniej sekcji wyjaśniono podstawowe zasady projektowania i wzorce projektowania modelu domeny. Teraz nadszedł czas, aby zapoznać się z możliwymi sposobami implementacji modelu domeny przy użyciu platformy .NET (zwykłego kodu C#) i platformy EF Core. Model domeny będzie składał się po prostu z kodu. Będzie to miało tylko wymagania dotyczące modelu EF Core, ale nie rzeczywiste zależności od platformy EF. Nie należy mieć twardych zależności ani odwołań do platformy EF Core ani żadnego innego rozwiązania ORM w modelu domeny.
Struktura modelu domeny w niestandardowej bibliotece .NET Standard
Organizacja folderów używana dla aplikacji referencyjnej eShopOnContainers demonstruje model DDD dla aplikacji. Może się okazać, że inna organizacja folderów bardziej wyraźnie komunikuje wybory projektowe dokonane dla aplikacji. Jak widać na rysunku 7–10, w modelu domeny zamawiania istnieją dwie agregacje, agregacja zamówień i agregacja nabywcy. Każda agregacja jest grupą jednostek domeny i obiektów wartości, chociaż można mieć agregację składającą się z pojedynczej jednostki domeny (agregującej jednostki głównej lub głównej), jak również.
Widok Eksplorator rozwiązań projektu Ordering.Domain przedstawiający folder AggregatesModel zawierający folder BuyerAggregate i OrderAggregate, każdy zawierający klasy jednostek, pliki obiektów wartości itd.
Rysunek 7–10. Struktura modelu domeny dla mikrousługi porządkowania w eShopOnContainers
Ponadto warstwa modelu domeny zawiera kontrakty repozytorium (interfejsy), które są wymaganiami infrastruktury modelu domeny. Innymi słowy, te interfejsy wyrażają, jakie repozytoria i metody muszą implementować warstwa infrastruktury. Ważne jest, aby implementacja repozytoriów została umieszczona poza warstwą modelu domeny w bibliotece warstwy infrastruktury, więc warstwa modelu domeny nie jest "zanieczyszczona" przez interfejs API ani klasy z technologii infrastruktury, takich jak Entity Framework.
Można również wyświetlić folder SeedWork zawierający niestandardowe klasy bazowe, których można użyć jako podstawy dla jednostek domeny i obiektów wartości, aby nie mieć nadmiarowego kodu w klasie obiektów każdej domeny.
Agregacja struktury w niestandardowej bibliotece .NET Standard
Agregacja odwołuje się do klastra obiektów domeny zgrupowanych razem w celu dopasowania do spójności transakcyjnej. Te obiekty mogą być wystąpieniami jednostek (z których jedna jest zagregowaną jednostką główną lub główną) oraz wszelkimi dodatkowymi obiektami wartości.
Spójność transakcyjna oznacza, że agregacja musi być spójna i aktualna na końcu działania biznesowego. Na przykład agregacja zamówień z modelu domeny zamówień mikrousług eShopOnContainers składa się, jak pokazano na rysunku 7-11.
Szczegółowy widok folderu OrderAggregate: Address.cs jest obiektem wartości, IOrderRepository jest interfejsem repozytorium, Order.cs jest elementem głównym agregacji, OrderItem.cs jest jednostką podrzędną, a OrderStatus.cs jest klasą wyliczenia.
Rysunek 7–11. Agregacja zamówień w rozwiązaniu programu Visual Studio
Jeśli otworzysz dowolny z plików w folderze agregacji, zobaczysz, jak jest on oznaczony jako niestandardowa klasa bazowa lub interfejs, taki jak obiekt jednostki lub wartości, zgodnie z implementacją w folderze SeedWork .
Implementowanie jednostek domeny jako klas POCO
Model domeny można zaimplementować na platformie .NET, tworząc klasy POCO, które implementują jednostki domeny. W poniższym przykładzie klasa Order jest definiowana jako jednostka, a także jako zagregowany katalog główny. Ponieważ klasa Order pochodzi z klasy bazowej Entity, może ponownie używać wspólnego kodu powiązanego z jednostkami. Należy pamiętać, że te klasy bazowe i interfejsy są definiowane przez Ciebie w projekcie modelu domeny, więc jest to kod, a nie kod infrastruktury z ORM, taki jak 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
// ...
}
Należy pamiętać, że jest to jednostka domeny zaimplementowana jako klasa POCO. Nie ma żadnej bezpośredniej zależności od platformy Entity Framework Core ani żadnej innej struktury infrastruktury. Ta implementacja jest taka, jak powinna znajdować się w DDD, tylko kod C# implementuje model domeny.
Ponadto klasa jest ozdobiona interfejsem O nazwie IAggregateRoot. Ten interfejs jest pustym interfejsem, czasami nazywanym interfejsem znacznika, który służy tylko do wskazania, że ta klasa jednostki jest również agregowanym elementem głównym.
Interfejs znacznika jest czasami uważany za antywzór; jednak jest to również czysty sposób oznaczania klasy, zwłaszcza gdy ten interfejs może się rozwijać. Atrybut może być innym wyborem dla znacznika, ale szybsze jest wyświetlanie klasy bazowej (jednostki) obok interfejsu IAggregate zamiast umieszczania znacznika atrybutu Aggregate nad klasą. Jest to kwestia preferencji, w każdym razie.
Posiadanie zagregowanego katalogu głównego oznacza, że większość kodu związanego ze spójnością i regułami biznesowymi jednostek agregacji powinna zostać zaimplementowana jako metody w klasie głównej Agregacja zamówień (na przykład AddOrderItem podczas dodawania obiektu OrderItem do agregacji). Nie należy tworzyć ani aktualizować obiektów OrderItems niezależnie ani bezpośrednio; Klasa AggregateRoot musi zachować kontrolę i spójność każdej operacji aktualizacji względem jej jednostek podrzędnych.
Hermetyzowanie danych w jednostkach domeny
Typowym problemem w modelach jednostek jest to, że uwidaczniają właściwości nawigacji kolekcji jako publicznie dostępne typy list. Dzięki temu każdy współpracownik może manipulować zawartością tych typów kolekcji, co może pomijać ważne reguły biznesowe związane z kolekcją, co może spowodować pozostawienie obiektu w nieprawidłowym stanie. Rozwiązaniem jest uwidacznienie dostępu tylko do odczytu powiązanych kolekcji i jawne udostępnienie metod definiujących sposoby manipulowania nimi przez klientów.
W poprzednim kodzie należy pamiętać, że wiele atrybutów jest tylko do odczytu lub prywatnych i można je aktualizować tylko za pomocą metod klasy, więc każda aktualizacja uwzględnia niezmienne domeny biznesowej i logikę określoną w metodach klasy.
Na przykład następujące wzorce DDD nie należy wykonywać następujących czynności z żadnej metody obsługi poleceń ani klasy warstwy aplikacji (w rzeczywistości nie powinno to być możliwe):
// 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);
//...
W tym przypadku metoda Add jest wyłącznie operacją dodawania danych z bezpośrednim dostępem do kolekcji OrderItems. W związku z tym większość logiki domeny, reguł lub walidacji związanych z tą operacją z jednostkami podrzędnymi zostanie rozłożona na warstwę aplikacji (programy obsługi poleceń i kontrolery internetowego interfejsu API).
Jeśli przejdziesz wokół zagregowanego katalogu głównego, element główny agregacji nie może zagwarantować jego niezmienności, jego ważności lub spójności. W końcu będziesz mieć kod spaghetti lub transakcyjny kod skryptu.
Aby postępować zgodnie ze wzorcami DDD, jednostki nie mogą mieć publicznych elementów ustawiających w żadnej właściwości jednostki. Zmiany w jednostce powinny być oparte na jawnych metodach z jawnym wszechobecnym językiem dotyczącym zmiany wykonywanej w jednostce.
Ponadto kolekcje w jednostce (na przykład elementy zamówienia) powinny być właściwościami tylko do odczytu (metoda AsReadOnly wyjaśniona później). Powinno być możliwe zaktualizowanie jej tylko z poziomu zagregowanych metod klasy głównej lub metod jednostki podrzędnej.
Jak widać w kodzie elementu głównego agregacji zamówienia, wszystkie zestawy powinny być prywatne lub co najmniej tylko do odczytu zewnętrznie, aby każda operacja względem danych jednostki lub jej jednostek podrzędnych musi być wykonywana za pomocą metod w klasie jednostki. Zapewnia to spójność w kontrolowany i obiektowy sposób zamiast implementowania kodu skryptu transakcyjnego.
Poniższy fragment kodu przedstawia odpowiedni sposób kodowania zadania dodawania obiektu OrderItem do agregacji 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.
//...
W tym fragmencie kodu większość weryfikacji lub logiki związanej z tworzeniem obiektu OrderItem będzie pod kontrolą elementu głównego agregacji order —w metodzie AddOrderItem — zwłaszcza walidacji i logiki powiązanej z innymi elementami w agregacji. Na przykład możesz uzyskać ten sam element produktu w wyniku wielu wywołań funkcji AddOrderItem. W tej metodzie można zbadać elementy produktu i skonsolidować te same elementy produktu w jeden obiekt OrderItem z kilkoma jednostkami. Ponadto, jeśli istnieją różne kwoty rabatu, ale identyfikator produktu jest taki sam, prawdopodobnie zastosujesz wyższy rabat. Ta zasada ma zastosowanie do dowolnej innej logiki domeny dla obiektu OrderItem.
Ponadto nowa operacja OrderItem(params) będzie również kontrolowana i wykonywana przez metodę AddOrderItem z katalogu głównego agregacji Order. W związku z tym większość logiki lub walidacji związanych z operacją (zwłaszcza wszystkie elementy wpływające na spójność między innymi jednostkami podrzędnymi) będą znajdować się w jednym miejscu w ramach agregowanego katalogu głównego. Jest to ostateczny cel zagregowanego wzorca głównego.
W przypadku korzystania z programu Entity Framework Core 1.1 lub nowszego jednostka DDD może być lepiej wyrażona, ponieważ umożliwia mapowanie pól poza właściwościami. Jest to przydatne podczas ochrony kolekcji jednostek podrzędnych lub obiektów wartości. Dzięki temu ulepszeniu można użyć prostych pól prywatnych zamiast właściwości i zaimplementować dowolną aktualizację do kolekcji pól w metodach publicznych i zapewnić dostęp tylko do odczytu za pomocą metody AsReadOnly.
W DDD chcesz zaktualizować jednostkę tylko za pomocą metod w jednostce (lub konstruktorze), aby kontrolować wszelkie niezmienne i spójność danych, więc właściwości są definiowane tylko przy użyciu metody get dostępu. Właściwości są wspierane przez pola prywatne. Dostęp do prywatnych składowych można uzyskać tylko z poziomu klasy. Istnieje jednak jeden wyjątek: program EF Core musi również ustawić te pola (aby można było zwrócić obiekt z odpowiednimi wartościami).
Mapowanie właściwości z dostępem tylko do pól w tabeli bazy danych
Mapowanie właściwości do kolumn tabeli bazy danych nie jest obowiązkiem domeny, ale częścią infrastruktury i warstwy trwałości. W tym miejscu wspominamy tylko o nowych funkcjach w programie EF Core 1.1 lub nowszym związanych z modelem jednostek. Dodatkowe szczegóły dotyczące tego tematu opisano w sekcji infrastruktury i trwałości.
W przypadku korzystania z programu EF Core 1.0 lub nowszego w obiekcie DbContext należy zamapować właściwości zdefiniowane tylko z metodami getters do rzeczywistych pól w tabeli bazy danych. Jest to wykonywane za pomocą metody HasField klasy PropertyBuilder.
Mapowanie pól bez właściwości
Funkcja w programie EF Core 1.1 lub nowszym w celu mapowania kolumn na pola jest również możliwa, aby nie używać właściwości. Zamiast tego możesz po prostu mapować kolumny z tabeli na pola. Typowym przypadkiem użycia jest pole prywatne dla stanu wewnętrznego, do którego nie trzeba uzyskiwać dostępu spoza jednostki.
Na przykład w poprzednim przykładzie kodu OrderAggregate istnieje kilka pól prywatnych, takich jak _paymentMethodId
pole, które nie mają powiązanej właściwości dla metody ustawiającej lub getter. To pole może być również obliczane w ramach logiki biznesowej zamówienia i używane z metod zamówienia, ale musi być również utrwalane w bazie danych. Dlatego w programie EF Core (od wersji 1.1) istnieje sposób mapowania pola bez powiązanej właściwości z kolumną w bazie danych. Wyjaśniono to również w sekcji Warstwa infrastruktury w tym przewodniku.
Dodatkowe zasoby
Vaughn Vernon. Modelowanie agregacji za pomocą DDD i Entity Framework. Należy pamiętać, że nie jest to program Entity Framework Core.
https://kalele.io/blog-posts/modeling-aggregates-with-ddd-and-entity-framework/Julie Lerman. Punkty danych — kodowanie dla projektowania opartego na domenie: Wskazówki dla deweloperów skoncentrowanych na danych
https://learn.microsoft.com/archive/msdn-magazine/2013/august/data-points-coding-for-domain-driven-design-tips-for-data-focused-devsUdi Dahan. Jak utworzyć w pełni hermetyzowane modele domen
https://udidahan.com/2008/02/29/how-to-create-fully-encapsulated-domain-models/Steve Smith. Jaka jest różnica między celami DTO i POCO? \ https://ardalis.com/dto-or-poco/