Jak zaimplementować Polityki z DDD w C#
Polityka to Building Block z DDD, który pozwala otwierać model na rozbudowę w tych miejscach, w których biznes może tego faktycznie potrzebować. Pojedyncze Polityki zwykle są stosunkowo proste, ich kompozycja może jednak modelować bardzo złożone wymagania biznesowe. Jak zaimplementować je w C#, żeby maksymalnie zwiększy czytelność?
DDD-starter-dotnet
Ten post jest częścią projektu DDD-starter-dotnet. Zachęcamy do obserwowania:
- Kodu: https://github.com/itlibrium/DDD-starter-dotnet
- Bloga: https://itlibrium.com/blog?tag=DDD-starter
- Twittera: ITLIBRIUM, Marcin, Szymon
Co modelować przez Polityki ?
Wyobraźmy sobie sytuację, w której daną wartość można wyznaczyć na wiele różnych sposobów. Przykładowo naliczanie podatku, w zależności od państwa (różnica per wdrożenie), lub wybranej formy opodatkowania (różnica per żądanie). Albo sytuację, w której obliczenie pewnej wartości wymaga złożenia wielu osobnych operacji i jest spore prawdopodobieństwo, że sposób tego złożenia będzie się zmieniać w przyszłości. Przykładowo naliczanie rabatu wymagające osobnego uwzględnienia promocji per produkt, warunków handlowych klienta, ustaleń o minimalnej marży, etc. Czy takie sytuacje zdarzają się w biznesie? Oczywiście i to dość często, szczególnie w jego kluczowych obszarach.
Polityki to właśnie model tego typu obliczania / wyznaczania wartości istotnych biznesowo. Modelowanie tego typu logiki osobno, poza zachowaniami Agregatów lub procesami biznesowymi znajdującymi się w Serwisach Aplikacyjnych / Domenowych daje modelowi elastyczność i otwiera go na rozbudowę. Żeby zastosować Politykę trzeba najpierw zidentyfikować, czy zachodzą powyższe warunki. Jeżeli tak, to warto w tym miejscu zadbać o elastyczność, jeżeli nie, to nie ma to sensu. Polityka nie jest więc czysto technicznym wzorcem strategii, chociaż wzorzec ten można wykorzystać do jej implementacji.
Polityki różnią się więc od Serwisów domenowych tym, że reprezentują zachowanie, które może być realizowane na kilka różnych sposobów, a nie jest sztywno określoną sekwencją czynności odwzorowującą proces biznesowy. Różnią się tym od Agregatów, że są bezstanowe, a co za tym idzie nie odpowiadają za ochronę reguł związanych ze spójnością długo żyjących danych.
Polityki mogą być wykorzystywane przez inne Building Blocki. Mogą być użyte bezpośrednio w ramach modelu procesów biznesowych (Serwisy domenowe) lub Use Case'ów (Serwisy aplikacyjne). Mogą być również przekazywane do zachowań Agregatów. W każdym przypadku "dostrajają" one zachowania innych komponentów aplikacji i explicite modelują to, co najbardziej zmienne w biznesie.
Jak zwiększać ekspresywność?
Obiekty to nie zawsze rzeczowniki
Skoro Polityki to model zachowań, to nie muszą one koniecznie być nazywane przy pomocy rzeczowników. Bardzo często czasowniki pasują tu dużo lepiej, co pozwala zmniejszyć rozdźwięk między językiem biznesu, a kodem. Porównajmy:
private DiscountApplier _discountApplier;
var newOffer = _discountApplier.Apply(offer);
private InvoiceIssuer _invoiceIssuer;
var invoice = _invoiceIssuer.Issue(order);
private ApplyDiscount _applyDiscount;
var newOffer = _applyDiscount.On(offer);
private IssueInvoice _issueInvoice;
var invoice = _issueInvoice.For(order);
Nie jest to oczywiście reguła i zawsze należy dobierać takie nazwy, które będą najbardziej naturalnie oddawały język biznesu. Na to, które to będzie podejście, ma też wpływ wykorzystana składnia języka, która jest obiektywnym ograniczeniem. Czasami przy wykorzystaniu czasownika jako nazwy klasy, może nie dać się sensownie nazwać metody. W takim przypadku lepiej pozostać przy tradycyjnym rzeczowniku.
delegate vs. interface
Skoro Polityka to model zachowania, to może wykorzystać tu delegaty zamiast interfejsów i implementujących je klas? Jest to jak najbardziej dobry pomysł. Jeżeli coś ze swojej natury jest funkcją to wykorzystanie funkcyjnych aspektów C# jest jak najbardziej na miejscu.
Przykładowo:
public delegate Offer OfferModifier(Offer offer);
public static class Discounts
{
public static PricingMethod SimpleProductLevelDiscount(Discount discount) =>
offer => Offer.FromQuotes(offer.Quotes.Select(quote => quote.Apply(discount)));
}
zamiast:
public interface OfferModifier
{
Offer ApplyOn(Offer offer);
}
public class SimpleProductLevelDiscount : PricingMethod
{
private readonly Discount _discount;
public SimpleProductLevelDiscount(Discount discount) => _discount = discount;
public Offer ApplyOn(Offer offer) =>
Offer.FromQuotes(offer.Quotes.Select(quote => quote.Apply(_discount)));
}
To, która opcja jest lepsza zależy od kilku czynników. Przede wszystkim są to preferencje zespołu. Jeżeli składnia obiektowa jest dla wszystkich czytelniejsza, to nie ma sensu wprowadzać na siłę delegatów. Jeżeli podejście funkcyjne jest dla zespołu bardziej naturalne, to ta namiastka funkcyjności w C# może okazać się bardzo wygodna.
Składnia oparta o delegaty jest z pewnością bardziej zwięzła. Łatwiej jest też wykorzystać w niej nazwy będące czasownikami. Składnia z interfejsem jest na pewno bardziej rozpowszechniona, a więc łatwiejsza dla większości deweloperów. Jest też ona elastyczniejsza w sytuacjach, gdy Polityka potrzebuje więcej danych, lub zależności do wykonania swojego zadania.
Łączenie z Value Objectami
Polityki świetnie łączą się z poprzednio omawianym Building Blockiem, czyli Value Objectami. Parametry wejściowe i wyjściowe Polityk powinny być właśnie modelowane w ten sposób. Pomoże to uniknąć długich i niezrozumiałych list argumentów, ale nie jest to jedyna korzyść. Część operacji, które można by zaimplementować w Polityce może znacznie lepiej pasować do Value Objectu. Wykorzystując typy proste najprawdopodobniej przeoczymy taką sytuację. Dobry model ma tę właściwość, że sam zadaje dobre pytania i podpowiada, które miejsce jest najbardziej naturalne, żeby dodać dany fragment logiki.
Wydajność
Wykorzystując delegaty należy pamiętać o ukrytych dodatkowych alokacjach, które mają miejsce, gdy do lambda expression przekazujemy zmienne lokalne. Nie ma tu miejsca, żeby omówić ten temat szerzej, ale łatwo znaleźć dobre materiały na ten temat pod hasłami: closure allocation i delegate allocation.
W większości przypadków nie jest to istotne, ale jeżeli najbardziej obciążone ścieżki w systemie wymagają niskich opóźnień, to warto wziąć ten temat pod uwagę. Można tu też dokonać pewnych optymalizacji wykorzystując struktury, ale o tym napiszę w jednym z kolejnych artykułów.
Podsumowanie
Do czego mogą się przydać Polityki?
- Do modelowania obliczania / wyznaczania wartości, które może być realizowane na wiele sposobów
- Do modelowania zachowań składających się z wielu kroków, gdy zasady kompozycji mogą ulegać częstym zmianom, lub są dynamiczne ze swej natury
- Do uelastyczniania modelu tam, gdzie biznes tego faktycznie potrzebuje
Jakie konstrukcje z C# można wykorzystać do implementacji Polityk?
- delegate + lambda expression
- interface + class lub struct
Przykłady implementacji Polityk możecie już teraz znaleźć na: https://github.com/itlibrium/DDD-starter-dotnet. W szczególności:
- Porównanie delegate vs. interface:
- Value Objecty wykorzystywane przez Polityki
W kolejnym artykule napiszę o tym jak wykorzystać Polityki w połączeniu z Fabrykami, żeby jeszcze zwiększyć elastyczność modelu i właściwie podzielić koncepcje domenowe.
Marcin Markowski
Architekt, trener, zwolennik podejścia Software Craftsmanship i ścisłej współpracy z biznesem. Specjalizuje się w modelowaniu opartym o Domain Driven Design i projektowaniu architektury systemów.
Zaczynał od consultingu biznesowego, później przeszedł do IT. Pracował zarówno nad systemami „enterprise”, jak i tworzył od podstaw rozwiązania dla małych firm. Próbował wejść w świat startupów z własnym produktem. Ostatecznie został jednak w IT, gdzie działa jako konsultant i trener.