Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Wskazówka
Ta treść jest fragmentem eBooka "Architektura mikrousług .NET dla konteneryzowanych aplikacji .NET", dostępnego na .NET Docs lub jako bezpłatny plik PDF do pobrania i czytania 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ą encji domeny i obiektów wartości, chociaż można mieć agregację składającą się z pojedynczej encji domeny (jednostki głównej, czyli korzenia agregacji lub encji głównej).
Widok Eksploratora rozwiązań dla projektu Ordering.Domain przedstawiający folder AggregatesModel zawierający folder BuyerAggregate i OrderAggregate, z których każdy zawiera klasy jednostek, pliki obiektów wartości itd.
Rysunek 7–10. Struktura modelu domeny dla mikrousługi zamówień 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.
Struktury agregowane w bibliotece niestandardowej .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 encji (z których jedna jest encją korzeniową) oraz 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 zbiorczym, zobaczysz, jak został oznaczony jako niestandardowa klasa bazowa lub interfejs, taki jak jednostka lub obiekt 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 korzeń agregatu. 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 korzenia agregatu oznacza, że większość kodu związanego ze spójnością i regułami biznesowymi elementów agregatu powinna zostać zaimplementowana jako metody w klasie korzenia agregatu Zamówienie (na przykład metoda AddOrderItem podczas dodawania obiektu OrderItem do agregatu). 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, zgodnie z wzorcami DDD, nie należy wykonywać poniższych czynności za pomocą żadnej metody obsługi poleceń ani klasy warstwy aplikacji (w rzeczywistości powinno to być niemożliwe do wykonania):
// 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 obejdziesz korzeń agregatu, korzeń agregatu nie może zagwarantować niezmienności, ważności ani spójności. W końcu będziesz mieć kod spaghetti lub kod skryptu transakcyjnego.
Aby postępować zgodnie ze wzorcami DDD, encje nie mogą mieć publicznych setterów w żadnej właściwości encji. Zmiany w jednostce powinny być oparte na jawnych metodach z jawnym wszechobecnym językiem dotyczącym zmiany wykonywanej w jednostce.
Ponadto zbiory wewnątrz jednostki (jak na przykład elementy zamówienia) powinny być właściwościami tylko do odczytu (metoda AsReadOnly zostanie 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 głównego elementu agregacji zamówienia, wszystkie settery powinny być prywatne lub co najmniej tylko do odczytu z zewnątrz, tak aby wszystkie operacje na danych jednostki lub jej jednostkach podrzędnych powinny być wykonywane za pomocą metod dostępnych 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ą korzenia agregatu Order, w metodzie AddOrderItem, zwłaszcza walidacji i logiki powiązanej z innymi elementami w agregacie. 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 korzenia agregacji Order. W związku z tym większość logiki lub walidacji związanych z operacją (zwłaszcza każdy element wpływający na spójność między innymi encytami podrzędnymi) będzie znajdować się w jednym miejscu w ramach korzenia agregatu. 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ć encję tylko za pomocą metod w encji (lub w konstruktorze), aby kontrolować wszelkie niezmienniki i spójność danych, więc właściwości są definiowane tylko za pomocą akcesora get. 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 EF Core 1.0 lub nowszej wersji, w ramach DbContext należy mapować właściwości, które są zdefiniowane tylko z getterami, 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
W EF Core 1.1 lub nowszym istnieje funkcja mapowania kolumn na pola, co umożliwia rezygnację z używania 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 jest kilka pól prywatnych, takich jak _paymentMethodId
pole, które nie mają powiązanej właściwości ani dla settera, ani gettera. 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 na potrzeby projektowania Domain-Driven: porady dla programistów Data-Focused
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 DTO a POCO? \ https://ardalis.com/dto-or-poco/