DDD - model domeny a warstwa persystencji

0

Cześć,

od pewnego czasu zgłębiam powyższy temat i o ile w teorii wszystko wydaje się piękne o tyle w praktyce podczas implementacji już nie.

Załóżmy poniższy model realizujący logikę BoundedContext: OrderManagement

    class Order : AggregateRoot
    {
        private readonly List<OrderItem> _items = new List<OrderItem>();
        public IEnumerable<OrderItem> Items => _items;

        public void AddProduct(Product product, int quantity)
        {
            _items.Add(new OrderItem(product, quantity));
        }

        public void DeleteProduct(Product product)
        {
            var item = _items.First(i => i.Product == product);

            _items.Remove(item);
        }
    }

    class OrderItem
    {
        public Product Product { get; }
        public int Quantity { get; }

        internal OrderItem(Product product, int quantity)
        {
            Product = product;
            Quantity = quantity;
        }
    }

    class Product
    {
        public int AvailableQuantity { get; }
        public int AvailableQuantityMax { get; }
    }

Jedno z wymagań do powyższego modelu to:

  • aktualizacja dostępnych sztuk produktu (AvailableQuantity) podczas dodawania, usuwania i edycji pozycji zamówienia.

Pierwotnie myślałem o dwóch metodach w encji Product o zakresie internal, które to odpowiednio zmieniają ilość dostępnych sztuk produktu.
Metody te chciałem wykorzystywać z poziomu AggregateRoot podczas AddProduct, DeleteProduct

Problemy pojawiły się podczas implementacji repozytorium dla AggregateRoot w zakresie operacji usuwania pozycji zamówienia oraz całego zamówienia.
Pod pojęciem usuwanie mam na myśli trwałe usunięcie tych danych z bazy danych (zostawmy dyskusję czemu lepiej nie kasować a oznaczać flagą).

Usuwając pozycję zamówienia tracę dostęp do obiektu Product, który to musi w warstwie persystencji zostać zapisany.

Jak powinienem zaimplementować taką fikcjonalność pamiętając o persistence ignorance?

1

co to znaczy dostępne?

1

Jeśli potrzebujesz bezpośredni dostęp do encji która wchodzi w skład AggregateRoota, to zwykle oznacza że trzeba z niej zrobić kolejnego AggregateRoota, problem solved.

3

Ah te crudy w DDD. Czy to będzie nowy rak w IT ?

0
neves napisał(a):

Jeśli potrzebujesz bezpośredni dostęp do encji która wchodzi w skład AggregateRoota, to zwykle oznacza że trzeba z niej zrobić kolejnego AggregateRoota, problem solved.

To nieprawda i nie jest to dobre rozwiązanie.
Dostęp do Encji czy Value Object możesz zapewnić przez Serwis nie musisz pobierać tego przez repozytorium.

Poza tym mylisz Encje z Value Object i widać to na kodzie, który pokazujesz. Wielokrotnie już pisałem, że jest to powszechne. Prawdopodobnie masz złe konteksty, bo prawdopodobnie jeden produkt to Aggregat a drugi jak sam widzisz Value Object.

1
error91 napisał(a):

Ah te crudy w DDD. Czy to będzie nowy rak w IT ?

Rakiem IT są ludzie. Równie dobrze można przypinać do projektu etykietę Agillity, bo akurat użyłem wzorca z tej książki.

0
neves napisał(a):

Jeśli potrzebujesz bezpośredni dostęp do encji która wchodzi w skład AggregateRoota, to zwykle oznacza że trzeba z niej zrobić kolejnego AggregateRoota, problem solved.

Z perspektywy modelu domeny nie potrzebuję takiego dostępu. Potrzebuję go dopiero podczas utrwalania zmian w warstwie persystencji.

0
Kombajnator napisał(a):
neves napisał(a):

Jeśli potrzebujesz bezpośredni dostęp do encji która wchodzi w skład AggregateRoota, to zwykle oznacza że trzeba z niej zrobić kolejnego AggregateRoota, problem solved.

Z perspektywy modelu domeny nie potrzebuję takiego dostępu. Potrzebuję go dopiero podczas utrwalania zmian w warstwie persystencji.

No to prawdopodobnie potrzebujesz IdentityField, który będzie w agregacie Order i Product, a ten produkt w agregacie Order zmień na ProductAvailability.

0
Gworys napisał(a):
Kombajnator napisał(a):
neves napisał(a):

Jeśli potrzebujesz bezpośredni dostęp do encji która wchodzi w skład AggregateRoota, to zwykle oznacza że trzeba z niej zrobić kolejnego AggregateRoota, problem solved.

Z perspektywy modelu domeny nie potrzebuję takiego dostępu. Potrzebuję go dopiero podczas utrwalania zmian w warstwie persystencji.

No to prawdopodobnie potrzebujesz IdentityField, który będzie w agregacie Order i Product, a ten produkt w agregacie Order zmień na ProductAvailability.

Nie rozumiem co masz na myśli. Nie wiem co zmienia wprowadzenie IdField oraz czemu w zamówieniu mam trzymać informację o pozostałej ilości dostępnych sztuk produktu.

2

Ach, czekaj nie doczytałem.

aktualizacja dostępnych sztuk produktu (AvailableQuantity) podczas dodawania, usuwania i edycji pozycji zamówienia.

To zrób usługę domenową, która będzie operować na dwóch agregatach. Order i Product

Albo niech repozytorium Order puści event dodano zamówienie a subskrybentem będzie usługa odejmująca ilość produktów.

0
Gworys napisał(a):

Ach, czekaj nie doczytałem.

aktualizacja dostępnych sztuk produktu (AvailableQuantity) podczas dodawania, usuwania i edycji pozycji zamówienia.

To zrób usługę domenową, która będzie operować na dwóch agregatach. Order i Product

Albo niech repozytorium Order puści event dodano zamówienie a subskrybentem będzie usługa odejmująca ilość produktów.

Nie jestem przekonany co do podejścia opartego o eventy. Bardziej szedłbym właśnie w stronę usługi domenowej.
Niezależnie od wybranej ścieżki, rodzi mi się w tym momencie kolejne pytanie, związane już bezpośrednio z samymi repozytoriami tworzonymi dla poszczególnych agregatów.
Gdzie powinna przebiegać ich granica odpowiedzialności za utrwalenie danych zwłaszcza gdy trzeba zachować spójność podczas transakcji?

Czy na zakończenie poszczególnych metod (Insert/Update/Delete ew. Save) w repozytorium powinienem wyzwalać Commit na bazie danych?
Początkowo myślałem, że tak ale teraz widzę iż nie jest to jakoś narzucone przez DDD i mogę w ramach danego BC utworzyć interfejs IUnitOfWork dzięki czemu mogę:

_orderRepository.Update(order);
_productRepository.Update(product);
_unitOfWork.Commit();
0

@Kombajnator

Repozytorium ma działać tak jakbyś coś, z niego wyciągał przez referencje.
Poza tym niby po co ci tam Update?

0
Gworys napisał(a):

@Kombajnator

Repozytorium ma działać tak jakbyś coś, z niego wyciągał przez referencje.
Poza tym niby po co ci tam Update?

Jak to poco?
Repozytorium służy przecież również do utrwalenia danych.
Wiem, że jeżeli korzystam z ORM to mogę przez metodę GetProduct zwrócić obiekt agregatu, który pozostanie "w trybie change tracking".
Nie wiem tego jednak po stronie warstwy logiki aplikacji.

W warstwie domeny mam definicję interfejsu repozytorium.
W warstwie infrastruktury mam implementację tegoż repozytorium.
W warstwie aplikacji (np. command hadler z CQRS) nie mogę zakładać, że obiekt agregatu zwrócony przez repo jest "w jakimś trybie śledzenia zmian".

Tak więc repozytorium musi mieć metody odpowiedzialne za zapis zmian.
Tak to przynajmniej rozumiem patrząc już z szerszej perspektywy.

0

Kurde @Kombajnator założyłeś temat, który i mnie jakiś czas temu interesował - miałem IDENTYCZNE pytania :D Nie znalazłem odpowiedzi :/
Repozytorium jest odpowiedzialne za odczyt **ORAZ ** zapis z/do jakiegoś magazynu danych:

public class ProductRepository
{
	private readonly DatabaseContext _databaseContext;

	public ProductRepository(DatabaseContext databaseContext)
	{
		_databaseContext = databaseContext;
	}

	public async Task<Product> GetProduct(Guid productId)
	{
		return await _databaseContext.Products.SingleAsync(x => x.ProductId == productId);
	}

	public async Task AddProduct(Product product)
	{
		await _databaseContext.Products.AddAsync(product);
                // dawac tu _databaseContext.Save() czy nie
	}
}

Pytanie czy wywoływać metodę Save() w repozytorium czy nie. W większości przykładów dostępnych w sieci - Save() jest wywoływany w repozytorium. Ale co w sytuacji jak zmiana ma miejsce w dwóch repozytoriach i wówczas dopiero chcemy zatwierdzić transakcję - unit of work tak jak zrobił autor wątku wydaje się sensowny. Ale ktoś ma może inne doświadczenia?

Druga kwestia, co z tym usuwaniem - w sieci ja też szukałem jakiś czas temu i nic nie znalazłem - przykładowo naszym agregatem jest zamówienie - z jakiegoś powodu chcielibyśmy usunąć zamówienie całkowicie z bazy danych. Czy dobrym pomysłem jest żeby agregat usuwał sam siebie w repozytorium?

0

Nie bardzo was rozumiem, jeśli stworzysz instancje "produktu" w serwisie z ORM'a to oznacza to, że ona przestaje istnieć w repozytorium i ORM'ie...? Albo w jakiś magiczny sposób jest odporna na zmiany, które wykonujesz wyżej w serwisie.?

A jaki problem rozwiązuje Unit Of Work.?

0

Czyli jak jednocześnie chcesz w transakcji modyfikować dane klienta i jego zamówienie to robisz to po prostu w serwisie?

0
wiewiorek napisał(a):

Czyli jak jednocześnie chcesz w transakcji modyfikować dane klienta i jego zamówienie to robisz to po prostu w serwisie?

Tak w domenowy, ale lepsze rozwiązanie jest te bazujące na eventach.

No i dlaczego nikt mnie nie poprawia.? Nawet dwa like za to dostałem :D

Tak na serio to powinieneś mieć tylko wspólny Value Object z ilością produktów dla dwóch agregatów Order i Product.

0
Gworys napisał(a):

Nie bardzo was rozumiem, jeśli stworzysz instancje "produktu" w serwisie z ORM'a to oznacza to, że ona przestaje istnieć w repozytorium i ORM'ie...? Albo w jakiś magiczny sposób jest odporna na zmiany, które wykonujesz wyżej w serwisie.?

A jaki problem rozwiązuje Unit Of Work.?

Nie chodzi o to, że przestaje istnieć lub jakąś inną magię :D
Chodzi o podział odpowiedzialności poszczególnych warstw.

Dla tego właśnie spytałem o granice odpowiedzialności implementacji repozytorium dla danego agregatu.

Przyjmijmy implementację repo. opartą o Entity Framework.
Repozytorium posiada metodę GetProduct, która zwraca obiekt żądanego agregatu.
Zmiany wprowadzone w tym obiekcie są "śledzone" przez mechanizm EF.
Można zatem po stronie logiki aplikacji wykonać taki kod:

var product = _repo.GetProduct(1);
product.GetAmount(8);
?????.SaveChanges();

Co jest nie tak z takim podejściem?
A no to, że zmiana implementacji repo pociąga za sobą zmiany w logice aplikacji (wywalasz EF i ożywasz ADO - SaveChanges przestaje działać).
Właśnie takich zależności między innymi chcemy unikać wprowadzając podziały na warstwy. Czyż nie?

Posiadając wspomnianą definicję interfejsu UoW po stronie logiki aplikacji mogę:

_orderRepository.Update(order);
_productRepository.Update(product);
_unitOfWork.Commit();

Kontener DI odpowiada za dostarczenie tej samej instancji DB Context'u zarówno do repozytoriów jak i "pod zmienną _unitOfWork".

namespace OrderManagement.Data.Repositories
{
    internal class OrderRepository : IOrderRepository
    {
        private readonly DatabaseContext _db;

        public OrderRepository(DatabaseContext db)
        {
            _db = db;
        }
		
		...
    }
}

namespace OrderManagement.Data
{
	internal class DatabaseContext : DbContext, IUnitOfWork
	{
		public void Commit() => Database.CurrentTransaction.Commit();
	}
}

namespace OrderManagement.App.Database
{
    public interface IUnitOfWork
    {
        void Commit();
    }
}

Dzięki temu zmiana implementacji warstwy persystencji nie pociąga za sobą zmiany w innych warstwach.

Gworys napisał(a):
wiewiorek napisał(a):

Czyli jak jednocześnie chcesz w transakcji modyfikować dane klienta i jego zamówienie to robisz to po prostu w serwisie?

Tak w domenowy, ale lepsze rozwiązanie jest te bazujące na eventach.

No i dlaczego nikt mnie nie poprawia.? Nawet dwa like za to dostałem :D

Tak na serio to powinieneś mieć tylko wspólny Value Object z ilością produktów dla dwóch agregatów Order i Product.

Możesz rozwinąć myśl (koncepcję) związaną z eventami?
Jak wygenerowanie eventu ma zapewnić spójność?

ValueObject tu? Hmm... To chyba niezbyt trafne zastosowanie.

0

ValueObject tu? Hmm... To chyba niezbyt trafne zastosowanie.

Dlaczego.? Mówiłeś coś wcześniej o abstrakcjach warstw, no więc dlaczego.? :D

A no to, że zmiana implementacji repo pociąga za sobą zmiany w logice aplikacji (wywalasz EF i ożywasz ADO - SaveChanges przestaje działać).
Właśnie takich zależności między innymi chcemy unikać wprowadzając podziały na warstwy. Czyż nie?

No bo trzeba wtedy samemu zaimplementować UOW. To że coś jest na dole zepsute to nie znaczy, że trzeba górę zmieniać.
Możesz sobie zrobić zamiast commit w UOW metodę Save zamiast Add w repozytorium, książkowo nazywa się to repozytorium typu magazyn trwały.

Możesz rozwinąć myśl (koncepcję) związaną z eventami?
Jak wygenerowanie eventu ma zapewnić spójność?

No niemoże a nawet nie powinno.

0
Gworys napisał(a):

ValueObject tu? Hmm... To chyba niezbyt trafne zastosowanie.

Dlaczego.? Mówiłeś coś wcześniej o abstrakcjach warstw, no więc dlaczego.? :D

Myślałem właśnie, że Ty mi uzasadnisz tą decyzję :)

When you care only about the attributes and logic of an element of the model, classify it as a value object. Make it express the meaning of the attributes it conveys and give it related functionality. Treat the value object as immutable.

When an object is distinguished by its identity, rather than its attributes, make this primary to its definition in the model. Keep the class definition simple and focused on life cycle continuity and identity.

Zmieniając ilość dostępnych sztuk nie zmieniam tożsamości produktu. Dla tego uważam, że powinien pozostać Encją.
Dla tego też przychylam się do opcji serwisu domenowego niż do ValueObject.

Gworys napisał(a):

A no to, że zmiana implementacji repo pociąga za sobą zmiany w logice aplikacji (wywalasz EF i ożywasz ADO - SaveChanges przestaje działać).
Właśnie takich zależności między innymi chcemy unikać wprowadzając podziały na warstwy. Czyż nie?

No bo trzeba wtedy samemu zaimplementować UOW. To że coś jest na dole zepsute to nie znaczy, że trzeba górę zmieniać.
Możesz sobie zrobić zamiast commit w UOW metodę Save zamiast Add w repozytorium, książkowo nazywa się to repozytorium typu magazyn trwały.

Posłużyłem się przykładem ADO aby zaznaczyć różnicę jaka może się pojawić gdy do komunikacji z bazą danych użyjemy zupełnie innego mechanizmu.
Implementuję warstwę persystencji przy użyciu ADO - sorry, moja świadoma decyzja o nakładzie pracy jaki mnie czeka.
Zmieniam użyty ORM w warstwie persystencji czy też samo miejsce zapisania zmian - też czeka nie zmiana implementacji repo ale tylko w jej granicach.

Przecież takie zmiany nie powinny implikować konieczności zmian po stronie innych warstw. Inaczej repozytorium implementowane na potrzeby DDD staje się abstrakcją na abstrakcję.
Przykładowy EF już sam w sobie jest implementacją Repozytorium jak i UoW.

Gworys napisał(a):

Możesz rozwinąć myśl (koncepcję) związaną z eventami?
Jak wygenerowanie eventu ma zapewnić spójność?

No niemoże a nawet nie powinno.

Czyli te podejście odpada :)

0

Problem polega na tym, że składanie zamówienia to nie jest banalna transakcją na dwóch tabelach.

Powinieneś pobrać ValueObject z np. kontekstu magazynu, który będzie zawierać id item'u w magazynie info o rozmiarze, wadze oraz ilości sztuk.

Po zakitowaniu płatności powinien być puszczony event z ilością sztuk do zmiany w magazynie.

Całość to proces długotrwały (Saga).

0

Nie komplikujmy problemu i pozostańmy przy prostym modelu :)

Dzięki za powyższy link.
Z tego co rozumiem, ValueObject ma nie tylko hermetyzować dostęp do pozycji zamówienia ale zawierać również informację o ilości sztuk dostępnych dla danego produktu?

class ProductOrder : ValueObject {
    public ProductID ProductID { get; }
    public int Quantity { get; }
    public int AvailableQuantity { get; }
} 

W agregacie Order mam metody za pomocą których dodaję, usuwam lub edytuję pozycje zamówienia.
Na koniec zmiany te muszę utrwalić więc przekazuję instancję agregatu do repo:

orderRepository.Save(order);

Sama implementacja repo już "wie" na których tabelach robić insert, update a na których delete.

0

No, byle byś nie zmieniał nic w tabeli, która należy do innych agregatów.

0

No dobra, jak teraz powinienem obsłużyć przypadek polegający na usunięciu pozycji zamówienia?
Chodzi mi o całą ścieżkę, począwszy od tego co się dzieje w agregacie Order zakończywszy na tym co robi metoda Save w OrderRepository.

1 użytkowników online, w tym zalogowanych: 0, gości: 1