To będzie trochę mechaniczny reverse-engineering Twojego kodu, ale może skorzystasz ;) Możesz spróbować zaaplikować ten sposób myślenia do pozostałych partii encji i zobaczymy, co wyjdzie. Disclaimer: nie znam Twojego projektu, nie wiem jaki biznes modelowałeś - to co napisałem może nie mieć w Twoim przypadku sensu.
Zaczynamy. Przykładowo, masz taką metodkę:
public Either<Success, DomainError> RequestDiscountApproval(string message)
co sugeruje, że masz jakiś feature polegający na zniżkach. Spróbujmy zamknąć to w osobnym bounded context. Stawiamy sobie za cel stan, w którym Offer
nic nie wie o zniżkach.
W tym celu zbieramy wszystkie commandy związane z Discountingiem
. Query na tym etapie mnie w ogóle nie interesują, ponieważ pesymistycznie można je ograć osobnym read modelem - skupiamy się na refactoringu modelu do zapisu. Przypominam, że model do zapisu to model skupiony na transakcyjnej ochronie niezmienników.
Commandy z domeny Discounting
wraz z odpowiadającymi niezmiennikami:
-
RequestDiscountApproval(string message)
-> DiscountStatus == DiscountStatus.ThresholdExceeded
-
ApproveDiscount()
-> DiscountStatus == DiscountStatus.SentForApproval
-
RejectDiscount(string reason)
-> DiscountStatus == DiscountStatus.SentForApproval)
-
SetDiscount(ProductId productId, Discount discount, CustomerMaxDiscount customerMaxDiscount)
-> !customerMaxDiscount.IsExceeded(item.UnitPrice, item.CalculateUnitPriceAfterDiscount(discount))
Kolejny krok - jakich danych potrzebujesz, żeby sprawdzić te niezmienniki. Wychodzi na to, że DiscountStatus
oraz jakieś info per customer. Pierwsze jest trywialne - dajesz pole w klasie, drugie również trywialne, bo przychodzi jako parametr metody ;D
Kolejna sprawa - te metody czytają, a nawet modyfikują jakieś pola z Offer
. Będziesz musiał poinformować tę encję o nałożonej zniżce i tutaj masz do wyboru 3 opcje:
- Asynchronicznie (czytaj dalej jak to zrobić).
- Synchronicznie - wysyłasz do oferty command
ApplyDiscount
.
- Zapisujesz zniżkę w
Discounting
i nie informujesz o zniżce, ale wówczas wszyscy klienci będą musieli wiedzieć, aby pobierając ceny oferty dodatkowo sprawdzić zniżki - bez sensu.
A zatem lądujemy z nową encją:
public class OfferDiscount : Entity
{
public OfferId Id { get; }
public DiscountStatus DiscountStatus { get; private set; }
RequestDiscountApproval(string message) { ... }
ApproveDiscount() { ... }
RejectDiscount(string reason) { ... }
SetDiscount(ProductId productId, Discount discount, CustomerMaxDiscount customerMaxDiscount) { ... }
}
Odpowiedzialność tej encji ogranicza się do automatu stanu - zamyka logikę sterującą, kiedy można nałożyć zniżkę. Benefit jest taki, że w przyszłości mogą być różne rodzaje zniżek, różnie uwarunkowanych, może proces ich zatwierdzania będzie bardziej złożony - masz to zamknięte w tym miejscu - super sprawa, możesz oddać to do osobnego zespołu :)
Dla kompletu można teraz dodać pozostałe pola: DiscountApprovalRequestMessage
i DiscountRejectionReason
- zapewne tylko na potrzeby odczytu, ponieważ nie występowały w warunkach. Takie rzeczy dorzucamy na sam koniec - tak jak pisałem wcześniej, najwyżej je sobie doczytasz, kiedy będziesz chciał renderować jakiś widok.
Musimy jeszcze zadbać o komunikację w drugą stronę - zrobimy to asynchronicznie by default (jak mawiał T.Nurkiewicz). Przykładowo, masz taką metodkę wołaną z innych metod publicznych:
private RefreshDiscountStatus()
Ona spokojnie może być wołana eventem, ponieważ nie blokuje flow innych commandów. Zamiast wołać ją bezpośrednio np. z ChangeProductPrice()
- rzuć event ProductPriceChanged
i zasubskrybuj się na niego w Discountingu
. Jeżeli to jednak byłoby częścią jakiegoś warunku/niezmiennika w innym bounded context - zawsze możesz odpytać ją synchronicznie (sync when necessary, jak mawiał T.Nurkiewicz). EDIT: po dokładniejszej analizie myślę, że wywołanie tej metody jest zbędne, natomiast mechanika pozostaje taka sama.
Doszliśmy do momentu, kiedy mamy już 2 BC: Core
(jakoś trzeba to nazwać) oraz Discounting
. Widzę, że kolejnym kandydatem rzucającym się w oko jest Notifications
.
To co wychodzi mi na event stormingach to zwykle to, że system można podzielić na wiele BC tak długo, jak nie mamy do czynienia z operacjami typu compare-and-swap (CAS). Wtedy jest to bardziej tricky i trzeba modelować wokół tego. Na szczęście jest na to wzorzec i nazywa się chyba Inventory (przedstawia go S.Sobótka w swoich wystąpieniach o DDD).
Mam nadzieję, że to ćwiczenie było dla Ciebie pomocne. Spróbuj tak zrobić z pozostałymi commandami - pogrupuj, poszukaj domen, zrób nowe agregaty. Iteruj i zobacz dokąd dojdziesz ;) DDD jest fajne. Wesołych Świąt!