Taktický Domain Driven Design quick overview
Tento článek je pouze rychlým přehledem nejdůležitějších částí taktického DDD a slouží pouze jako doprovod k následujícímu článku
Domain driven design (DDD) se dělí na dvě hlavní části – taktickou a strategickou. Strategická část se zabývá rozpadem systému do menších oblastí (mimo jiné). Taktická část se zabývá samotnou implementací jednotlivých částí systému. V tomto a dalším článku se zaměříme pouze na taktickou část DDD.
Taktické DDD
Doména
Ústřední myšlenkou DDD je Doména – část aplikace, která není závislá na žádné technologii a která obsahuje všechny doménové operace. Například v e-shopu jsou doménovými operacemi: “přidat produkt do košíku”, “odebrat produkt z košíku”, “objednání košíku” atd.
V určitém smyslu je tedy doména ta nejdůležitější část vaší aplikace. Jakési srdce, které je důležitější než zbytek softwaru. Všechno ostatní je s trochou nadsázky pouze implementační detail.
Stejnou důležitost dávají této části aplikace i lidé, kteří vám zadávají práci. Obvykle nemluví o tom, jak ukládat data nebo jak poslat e-mail. To pro ně není důležité. Důležité je, že se e-mail odešle, když se uživatel zaregistruje. Jak se to stane, už není důležité.
Proces odeslání e-mailu je tedy součást domény, ale jak se samotné odeslání stane, je už detail, který by měl v aplikaci existovat mimo doménu. V praxi můžete tuto skutečnost namodelovat například následujícím způsobem:
// Interface part of the domain
public interface IEmailSender
{
public Task Send(string to, string subject, string body);
}
// Implementation part of the infrastructure
public class EmailSender : IEmailSender
{
public Task Send(string to, string subject, string body)
{
// Email sending logic
}
}Infrastruktura a doména pak mohou být v odlišných složkách nebo třeba projektech, pokud pracujete v .NET.
Entity, value objecty a doménové služby
Eric Evans (autor DDD) také definoval tři pojmy, které vám mají pomoci popsat doménu pomocí kódu. Jsou to – entity, value objecty a doménové služby. Entity a value objecty si můžete jednoduše představit jako objekty z OOP, které odpovídají doménovým konceptům.
Například v doméně e-shopu mohou existovat entity – User, Cart, Item, Order atd. Tyto objekty se skládají z dat a operací nad těmito daty. Například:
// constructor omitted for brevity
public class User
{
public int Id { get; private set;}
public string Name { get; private set;}
private List<Order> Orders { get; private set; } = new List<Order>();
public void RemoveOrder(Order order)
{
// Some logic about removing order
}
//...
}DDD ale rozlišuje ještě entity a value objecty – oboje jsou objekty, které drží data a operace nad nimi. Rozdíl je v jejich identitě. Nejlépe se dá rozdíl ukázat na příkladu:
- User je entita, jelikož 2 lidé, kteří mají naprosto stejné vlastnosti (adresa, počet objednávek, obsah košíku…), jsou stále dva rozdílní lidé.
- Adresa je value object, jelikož dvě adresy, které mají stejná data (město, ulici, PSČ, číslo domu), jsou identické.
V kódu se value object a entita liší pouze tak, že entita má Id a value object má immutable data a přetíženou operaci equals.
Příklad value objectu:
// constructor omitted for brevity
public class Address
{
public string Street { get; private set; }
public string City { get; private set; }
public string PostCode { get; private set; }
public string Country { get; private set; }
public override bool Equals(object obj)
{
return obj is Address address &&
Street == address.Street &&
City == address.City &&
PostCode == address.PostCode &&
Country == address.Country;
}
}Poslední typ objektu, který existuje v doméně aplikace, je “doménová služba”. DDD preferuje dávat veškerou logiku do entit a value objectů. V některých případech se ale může stát, že doménová logika do žádné nepasuje. V tu chvíli je možné vytvořit nový objekt doménové služby, který provede danou operaci. Doménové služby nedrží žádná data. Obsahují pouze operace nad entitami a value objecty.
Doménové invarianty
Eric Evans (autor DDD) popsal také koncept “doménových invariant”, což jsou pravidla, která musí ve vaší doméně vždy platit. Například – “uživatel musí být starší než 18 let” nebo “uživatel nesmí do košíku vložit více než 3 produkty”.
Tyto invarianty jsou kontrolovány při vytváření entit a value objectů a také při provádění operací nad nimi. Důležité je, že invarianty musí platit VŽDY, abyste se na ně mohli spolehnout. Například následující kód by byl z pohledu DDD považován za nesprávný
// Lets assume we have invariant - "User age must be greater than 18"
var user = new User(age: 15);
user.ValidateAge()jelikož uživatel může být vytvořen s věkem nižším než 18 let, pokud někdo zapomene zavolat metodu ValidateAge. Lepším řešením z pohledu DDD je provést validaci už v konstruktoru uživatele:
var user = new User(age: 15); // throws exceptionTímto způsobem zajistíme, že invarianty nikdy nebudou porušeny a další kód se může vždy spoléhat na jejich platnost.
Aggregate, Aggregate root a repository
Zajištění invariantů je jednoduché pro situace, kdy nám stačí data pouze z jedné entity. Jak ale vyřešit situace kde potřebujeme data z více entit? Například invariantu - “Pouze prémiový uživatel může dát do košíku více než 3 produkty”?
Naše doména by mohla vypadat třeba takto:
// constructor omitted for brevity
public class User
{
public int Id { get; private set;}
public bool IsPremium { get; private set; }
public Cart Cart { get; private set;}
public void AddProductToCart(Product product)
{
if (Cart.Products.Count == 3 && !IsPremium)
{
throw new InvalidOperationException("User can not add more than 3 products to cart");
}
Cart.Products.Add(product);
}
}
public class Cart
{
public int Id { get; private set;}
public User User { get; private set; }
public List<Product> Products { get; set;} = new List<Product>();
}Toto řešení vypadá v pořádku, ale obsahuje jeden velký problém – programátor může jednoduše porušit invarianty:
// Load cart for non premium user from database
var cart = cartRepository.GetCartForUser(1);
// Add as many products as you want to cart
cart.Products.Add(new Product());
cart.Products.Add(new Product());
cart.Products.Add(new Product());
cart.Products.Add(new Product());DDD tento problém řeší pomocí Aggregátu – skupinou objektů, se kterou pracujeme společně pomocí aggregate rootu. V našem příkladu vytvoříme z entit User a Cart aggregate a User vybereme jako aggregate root. Poté můžeme zajistit invarianty následujícím způsobem:
// constructor omitted for brevity
public class User
{
public int Id { get; private set;}
public bool IsPremium { get; private set; }
// Cart is now fully private so it can not be accessed directly from outside
private Cart Cart { get; set; }
public void AddProductToCart(Product product)
{
if (Cart.Products.Count == 3 && !IsPremium)
{
throw new InvalidOperationException("User can not add more than 3 products to cart");
}
Cart.Products.Add(product);
}
}Tímto jsme zajistili, že nikdo nepřistoupí ke košíku pomocí Cart objektu přímo. Stále ale můžeme načíst Cart z databáze a upravit ho.
DDD tento problém řeší tak, že pro načítání dat z databáze potřebujeme “Repository” a Repository existují pouze pro aggregate rooty a ne pro samostatné entity.
V aplikaci bychom tedy měli něco jako:
public class UserRepository
{
public User GetById(int id);
public void Save(User user);
public void Update(User user);
}Závěr
Tento článek sloužil pouze jako rychlé shrnutí nejdůležitějších konceptů DDD, aby vám pomohl pochopit další článek Taktický Domain Driven Design nefunguje. Doufám, že vám pomohl pochopit 2 hlavní myšlenky – modelování domény a zajišťování invariantů.