Implementace doménového modelu mikroslužby pomocí .NET
Tip
Tento obsah je výňatek z eBooku, architektury mikroslužeb .NET pro kontejnerizované aplikace .NET, které jsou k dispozici na .NET Docs nebo jako zdarma ke stažení PDF, které lze číst offline.
V předchozí části byly vysvětleny základní principy návrhu a vzory návrhu doménového modelu. Teď je čas prozkoumat možné způsoby implementace doménového modelu pomocí .NET (prostý kód jazyka C#) a EF Core. Váš doménový model se bude skládat jednoduše z vašeho kódu. Bude mít pouze požadavky na model EF Core, ale ne skutečné závislosti na EF. V doménovém modelu byste neměli mít pevné závislosti ani odkazy na EF Core ani žádné jiné ORM.
Struktura doménového modelu ve vlastní knihovně .NET Standard
Organizace složek používaná pro referenční aplikaci eShopOnContainers ukazuje model DDD pro aplikaci. Možná zjistíte, že jiná organizace složek jasněji komunikuje s možnostmi návrhu vaší aplikace. Jak vidíte na obrázku 7–10, v modelu objednávání domén existují dvě agregace, agregace objednávek a agregace kupujícího. Každá agregace je skupina entit domény a objektů hodnot, i když byste mohli mít agregaci složenou z jedné entity domény (agregované kořenové nebo kořenové entity).
Zobrazení Průzkumník řešení pro projekt Ordering.Domain zobrazující složku AggregatesModel obsahující složky KupujícíAggregate a OrderAggregate, z nichž každý obsahuje své třídy entit, soubory objektů hodnot atd.
Obrázek 7–10 Struktura doménového modelu pro objednávkovou mikroslužbu v eShopOnContainers
Vrstva doménového modelu navíc zahrnuje kontrakty úložiště (rozhraní), které jsou požadavky na infrastrukturu vašeho doménového modelu. Jinými slovy, tato rozhraní vyjadřují, jaká úložiště a metody musí vrstva infrastruktury implementovat. Je důležité, aby implementace úložišť byla umístěna mimo vrstvu doménového modelu v knihovně vrstev infrastruktury, takže vrstva doménového modelu není "kontaminovaná" rozhraním API nebo třídami z technologií infrastruktury, jako je Entity Framework.
Můžete také zobrazit složku SeedWork , která obsahuje vlastní základní třídy, které můžete použít jako základ pro entity domény a objekty hodnot, takže v každé třídě objektů domény nemáte redundantní kód.
Agregace struktur ve vlastní knihovně .NET Standard
Agregace odkazuje na cluster objektů domény seskupených dohromady tak, aby odpovídal transakční konzistenci. Tyto objekty můžou být instance entit (z nichž jedna je agregovaná kořenová nebo kořenová entita) a všechny další objekty hodnot.
Transakční konzistence znamená, že je zaručeno, že agregace bude konzistentní a aktuální na konci obchodní akce. Například agregace objednávek z doménového modelu mikroslužby eShopOnContainers se skládá, jak je znázorněno na obrázku 7–11.
Podrobné zobrazení složky OrderAggregate: Address.cs je objekt hodnoty, IOrderRepository je rozhraní úložiště, Order.cs je agregační kořen, OrderItem.cs je podřízená entita a OrderStatus.cs je třída výčtu.
Obrázek 7–11 Agregace objednávek v řešení sady Visual Studio
Pokud otevřete některý ze souborů v agregační složce, uvidíte, jak je označena jako vlastní základní třída nebo rozhraní, například objekt entity nebo hodnoty, jak je implementováno ve složce SeedWork .
Implementace entit domény jako tříd POCO
V .NET implementujete doménový model vytvořením tříd POCO, které implementují entity vaší domény. V následujícím příkladu je třída Order definována jako entita a také jako agregační kořen. Vzhledem k tomu, že order třída je odvozena ze základní třídy Entity, může znovu použít společný kód související s entitami. Mějte na paměti, že tyto základní třídy a rozhraní jsou definovány v projektu doménového modelu, takže se jedná o váš kód, nikoli kód infrastruktury z ORM, jako je 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
// ...
}
Je důležité si uvědomit, že se jedná o entitu domény implementovanou jako třídu POCO. Nemá žádnou přímou závislost na Entity Framework Core ani na žádné jiné infrastruktuře. Tato implementace je tak, jak by měla být v DDD, pouze kód jazyka C#, který implementuje doménový model.
Třída je navíc zdobena rozhraním s názvem IAggregateRoot. Toto rozhraní je prázdné rozhraní, někdy označované jako rozhraní značek, které slouží pouze k označení, že tato třída entity je také agregační kořen.
Rozhraní značky je někdy považováno za anti-vzor; Je to ale také čistý způsob, jak označit třídu, zejména když se toto rozhraní může vyvíjet. Atribut může být druhou volbou značky, ale je rychlejší zobrazit základní třídu (Entity) vedle rozhraní IAggregate místo vložení značky agregace atributu nad třídu. Je to věc předvoleb, v každém případě.
Agregační kořen znamená, že většina kódu souvisejícího s konzistencí a obchodními pravidly entit agregace by se měla implementovat jako metody v kořenové třídě Order aggregate (například AddOrderItem při přidání objektu OrderItem do agregace). Objekty OrderItems byste neměli vytvářet ani aktualizovat nezávisle nebo přímo; Třída AggregateRoot musí zachovat kontrolu a konzistenci jakékoli operace aktualizace vůči podřízeným entitám.
Zapouzdření dat v entitách domény
Běžným problémem v modelech entit je, že zpřístupňují navigační vlastnosti kolekce jako veřejně přístupné typy seznamů. To umožňuje každému vývojáři spolupracovníka manipulovat s obsahem těchto typů kolekcí, což může obejít důležitá obchodní pravidla související s kolekcí, případně ponechat objekt v neplatném stavu. Řešením je zveřejnit přístup jen pro čtení související kolekce a explicitně poskytovat metody definující způsoby, kterými s nimi můžou klienti manipulovat.
V předchozím kódu si všimněte, že mnoho atributů je jen pro čtení nebo soukromé a jsou aktualizovatelné pouze metodami třídy, takže každá aktualizace považuje invarianty obchodní domény a logiku zadanou v rámci metod třídy.
Například následující vzory DDD byste neměli provádět z žádné metody obslužné rutiny příkazů ani třídy aplikační vrstvy (ve skutečnosti by to nemělo být pro vás nemožné):
// 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);
//...
V tomto případě je metoda Add čistě operací pro přidání dat s přímým přístupem k kolekci OrderItems. Proto se většina logiky domény, pravidel nebo ověření souvisejících s danou operací s podřízenými entitami rozšíří do aplikační vrstvy (obslužné rutiny příkazů a kontrolery webového rozhraní API).
Pokud přejdete kolem kořenového adresáře agregace, agregační kořen nemůže zaručit jeho invarianty, jeho platnost nebo jeho konzistenci. Nakonec budete mít špagetový kód nebo kód transakčního skriptu.
Pokud chcete sledovat vzory DDD, entity nesmí mít veřejné settery v žádné vlastnosti entity. Změny v entitě by měly být řízeny explicitními metodami s explicitním všudypřítomným jazykem o změně, kterou v entitě provádějí.
Kromě toho by kolekce v entitě (podobně jako položky objednávky) měly být jen pro čtení vlastnosti (metoda AsReadOnly vysvětlena později). Měli byste být schopni ji aktualizovat pouze z agregovaných metod kořenové třídy nebo podřízených metod entity.
Jak vidíte v kódu pro kořenový adresář agregace Order, všechny setter by měly být soukromé nebo alespoň jen pro čtení externě, takže všechny operace s daty entity nebo jeho podřízenými entitami musí být provedeny prostřednictvím metod ve třídě entity. To udržuje konzistenci řízeným a objektově orientovaným způsobem místo implementace kódu transakčního skriptu.
Následující fragment kódu ukazuje správný způsob, jak zakódovat úlohu přidání orderItem objektu do agregace 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.
//...
V tomto fragmentu kódu bude většina ověření nebo logiky související s vytvořením objektu OrderItem pod kontrolou kořenového adresáře Order agregace – v metodě AddOrderItem – zejména ověření a logika související s jinými prvky v agregaci. Můžete například získat stejnou položku produktu jako výsledek více volání AddOrderItem. V této metodě byste mohli prozkoumat položky produktu a sloučit stejné položky produktu do jednoho objektu OrderItem s několika jednotkami. Pokud jsou navíc jiné částky slevy, ale ID produktu je stejné, pravděpodobně byste použili vyšší slevu. Tento princip se vztahuje na jakoukoli jinou logiku domény pro OrderItem objektu.
Kromě toho bude nová operace OrderItem(params) také řízena a provedena metodou AddOrderItem z agregovaného kořenového adresáře Order. Proto většina logiky nebo ověření související s danou operací (zejména cokoli, co ovlivňuje konzistenci mezi jinými podřízenými entitami), bude na jednom místě v rámci agregovaného kořenového adresáře. To je konečný účel agregovaného kořenového vzoru.
Pokud používáte Entity Framework Core 1.1 nebo novější, může být entita DDD lépe vyjádřena, protože umožňuje mapování na pole kromě vlastností. To je užitečné při ochraně kolekcí podřízených entit nebo objektů hodnot. Díky tomuto vylepšení můžete místo vlastností použít jednoduchá soukromá pole a můžete implementovat jakoukoli aktualizaci kolekce polí ve veřejných metodách a poskytnout přístup jen pro čtení prostřednictvím metody AsReadOnly.
V DDD chcete entitu aktualizovat pouze metodami v entitě (nebo konstruktoru), aby bylo možné řídit všechny invariantní a konzistence dat, takže vlastnosti jsou definovány pouze pomocí přístupového objektu get. Vlastnosti jsou podporovány privátními poli. K soukromým členům lze přistupovat pouze v rámci třídy. Existuje však jedna výjimka: EF Core potřebuje také nastavit tato pole (aby mohl vrátit objekt se správnými hodnotami).
Mapování vlastností pouze s přístupovými objekty k polím v tabulce databáze
Mapování vlastností na sloupce tabulek databáze není doménovou odpovědností, ale součástí infrastruktury a vrstvy trvalosti. Uvádíme to tady jen proto, abyste věděli o nových funkcích ef Core 1.1 nebo novějších souvisejících s tím, jak můžete modelovat entity. Další podrobnosti o tomto tématu jsou vysvětleny v části Infrastruktura a trvalost.
Pokud používáte EF Core 1.0 nebo novější, musíte v dbContext mapovat vlastnosti, které jsou definovány pouze pomocí getters na skutečná pole v tabulce databáze. To se provádí pomocí HasField Metoda PropertyBuilder třídy.
Mapování polí bez vlastností
Díky funkci EF Core 1.1 nebo novější pro mapování sloupců na pole je také možné použít vlastnosti. Místo toho můžete namapovat sloupce z tabulky na pole. Běžným případem použití pro toto jsou soukromá pole pro interní stav, ke kterému není potřeba přistupovat mimo entitu.
Například v předchozím příkladu kódu OrderAggregate existuje několik privátních polí, jako je _paymentMethodId
pole, které nemají žádnou související vlastnost pro setter nebo getter. Toto pole lze také vypočítat v obchodní logice objednávky a použít je z metod objednávky, ale je třeba ho zachovat i v databázi. V EF Core (od verze 1.1) je tedy způsob, jak mapovat pole bez související vlastnosti na sloupec v databázi. To je vysvětleno také v části Vrstva infrastruktury tohoto průvodce.
Další materiály
Vaughn Vernon. Modelování agregací pomocí DDD a Entity Frameworku Všimněte si, že to není Entity Framework Core.
https://kalele.io/blog-posts/modeling-aggregates-with-ddd-and-entity-framework/Julie Lermanová. Datové body – kódování návrhu řízeného doménou: Tipy pro vývojáře zaměřené na data
https://learn.microsoft.com/archive/msdn-magazine/2013/august/data-points-coding-for-domain-driven-design-tips-for-data-focused-devsUdi Dahan. Vytvoření plně zapouzdřených doménových modelů
https://udidahan.com/2008/02/29/how-to-create-fully-encapsulated-domain-models/Steve Smith. Jaký je rozdíl mezi DTO a POCO? \ https://ardalis.com/dto-or-poco/