Czy istnieje elegancki sposób sprawdzania unikalnych ograniczeń atrybutów obiektów domeny bez przenoszenia logiki biznesowej do warstwy usług?

10

Dostosowuję projektowanie oparte na domenie od około 8 lat i nawet po tylu latach wciąż mnie wkurza. To sprawdza unikalny rekord w przechowywaniu danych w stosunku do obiektu domeny.

We wrześniu 2013 r. Martin Fowler wspomniał o zasadzie TellDon'tAsk , która, jeśli to możliwe, powinna być stosowana do wszystkich obiektów domeny, która powinna następnie zwrócić komunikat o przebiegu operacji (w projektowaniu obiektowym odbywa się to głównie poprzez wyjątki, gdy operacja zakończyła się niepowodzeniem).

Moje projekty są zwykle podzielone na wiele części, przy czym dwie z nich to Domena (zawierająca reguły biznesowe i nic więcej, domena jest całkowicie ignorancka) i Usługi. Usługi znające warstwę repozytorium używaną do danych CRUD.

Ponieważ unikalność atrybutu należącego do obiektu jest domeną / regułą biznesową, moduł domeny powinien być długi, więc reguła jest dokładnie tam, gdzie powinna być.

Aby móc sprawdzić unikatowość rekordu, musisz zapytać o aktualny zestaw danych, zwykle bazę danych, aby dowiedzieć się, czy Nameistnieje już inny rekord z na przykład powiedzmy .

Biorąc pod uwagę, że warstwa domeny jest uporczywa w ignorancji i nie ma pojęcia, jak odzyskać dane, ale tylko jak wykonać na nich operacje, tak naprawdę nie może dotknąć samych repozytoriów.

Projekt, który następnie dostosowywałem, wygląda następująco:

class ProductRepository
{
    // throws Repository.RecordNotFoundException
    public Product GetBySKU(string sku);
}

class ProductCrudService
{
    private ProductRepository pr;

    public ProductCrudService(ProductRepository repository)
    {
        pr = repository;
    }

    public void SaveProduct(Domain.Product product)
    {
        try {
            pr.GetBySKU(product.SKU);

            throw Service.ProductWithSKUAlreadyExistsException("msg");
        } catch (Repository.RecordNotFoundException e) {
            // suppress/log exception
        }

        pr.MarkFresh(product);
        pr.ProcessChanges();
    }
}

Prowadzi to do tego, że usługi definiują reguły domeny zamiast samej warstwy domeny, a reguły są rozproszone po wielu sekcjach kodu.

Wspomniałem o zasadzie TellDon'tAsk, ponieważ jak widać, usługa oferuje akcję (zapisuje Productlub zgłasza wyjątek), ale w metodzie operujesz na obiektach przy użyciu podejścia proceduralnego.

Oczywistym rozwiązaniem jest utworzenie Domain.ProductCollectionklasy za pomocą Add(Domain.Product)metody rzucającej ProductWithSKUAlreadyExistsException, ale bardzo brakuje jej wydajności, ponieważ trzeba by uzyskać wszystkie Produkty z przechowywania danych, aby dowiedzieć się w kodzie, czy Produkt ma już tę samą SKU jako produkt, który próbujesz dodać.

Jak rozwiązujecie ten konkretny problem? Sam w sobie nie stanowi to problemu, ponieważ warstwa usług reprezentuje pewne reguły domenowe od lat. Warstwa usługowa zazwyczaj obsługuje również bardziej złożone operacje na domenach, po prostu zastanawiam się, czy natknąłeś się na lepsze, bardziej scentralizowane rozwiązanie podczas swojej kariery.

Andy
źródło
Po co wyciągać całą listę nazwisk, skoro można o nie poprosić i oprzeć logikę na tym, czy znaleziono? Mówisz warstwie trwałości, co robić, a nie jak to robić, o ile istnieje zgoda z interfejsem.
JeffO
1
Używając terminu Usługa, powinniśmy dążyć do większej jasności, takiej jak różnica między usługami domenowymi w warstwie domenowej a usługami aplikacyjnymi w warstwie aplikacyjnej. Zobacz gorodinski.com/blog/2012/04/14/…
Erik Eidt
Doskonały artykuł, @ErikEidt, dziękuję. Wydaje mi się, że mój projekt nie jest zły, jeśli mogę zaufać panu Gorodinskiemu, mówi on prawie to samo: Lepszym rozwiązaniem jest, aby usługa aplikacji pobierała informacje wymagane przez podmiot, skutecznie konfigurując środowisko wykonawcze i dostarczając do bytu i ma taki sam sprzeciw wobec wstrzykiwania repozytorium do modelu domeny bezpośrednio jak ja, głównie przez złamanie SRP.
Andy
Cytat z odniesienia „powiedz, nie pytaj”: „Ale osobiście nie używam tell-dont-ask”.
radarbob

Odpowiedzi:

7

Biorąc pod uwagę, że warstwa domeny jest uporczywa w ignorancji i nie ma pojęcia, jak odzyskać dane, ale tylko jak wykonać na nich operacje, tak naprawdę nie może dotknąć samych repozytoriów.

Nie zgodziłbym się z tą częścią. Zwłaszcza ostatnie zdanie.

Chociaż prawdą jest, że domena powinna być ignorantem uporczywym, wie, że istnieje „Kolekcja podmiotów domeny”. I że istnieją reguły domeny dotyczące tej kolekcji jako całości. Wyjątkowość jest jednym z nich. A ponieważ implementacja rzeczywistej logiki w dużej mierze zależy od określonego trybu trwałości, musi istnieć pewien rodzaj abstrakcji w domenie, która określa potrzebę tej logiki.

Jest to więc tak proste, jak utworzenie interfejsu, który może zapytać, czy nazwa już istnieje, który jest następnie implementowany w magazynie danych i wywoływany przez osobę, która musi wiedzieć, czy nazwa jest unikalna.

Chciałbym podkreślić, że repozytoria to usługi DOMAIN. Są abstrakcjami wokół wytrwałości. Jest to implementacja repozytorium, które powinno być niezależne od domeny. Nie ma absolutnie nic złego w tym, że jednostka domeny wywołuje usługę domeny. Nie ma nic złego w tym, że jedna jednostka może korzystać z repozytorium w celu pobrania innej encji lub pobrania określonych informacji, których nie można łatwo przechowywać w pamięci. To jest powód, dla którego Repozytorium jest kluczową koncepcją w książce Evansa .

Euforyk
źródło
Dziękuję za wkład. Czy repozytorium może faktycznie reprezentować to, Domain.ProductCollectionco miałem na myśli, biorąc pod uwagę, że są one odpowiedzialne za pobieranie obiektów z warstwy domeny?
Andy,
@DavidPacker Naprawdę nie rozumiem, co masz na myśli. Ponieważ nie powinno być potrzeby przechowywania wszystkich elementów w pamięci. Implementacją metody „DoesNameExist” powinno być (najprawdopodobniej) zapytanie SQL po stronie magazynu danych.
Euforyczny
Oznacza to, że zamiast przechowywać dane w kolekcji w pamięci, gdy chcę poznać cały Produkt, od którego nie muszę go pobierać Domain.ProductCollection, zamiast tego pytam repozytorium, pytając, czy Domain.ProductCollectionzawiera Produkt z minęło SKU, tym razem ponownie pytając repozytorium zamiast tego (tak naprawdę jest to przykład), który zamiast iteracji nad wstępnie załadowanymi produktami odpytuje bazową bazę danych. Nie mam zamiaru przechowywać całego Produktu w pamięci, chyba że muszę, to byłoby kompletnym nonsensem.
Andy,
Co prowadzi do kolejnego pytania: czy repozytorium powinno wiedzieć, czy atrybut powinien być unikalny? Zawsze implementowałem repozytoria jako dość głupie komponenty, zapisując to, co przekazujesz, aby je zapisać i próbując odzyskać dane na podstawie przekazanych warunków i wprowadzić decyzję w usługi.
Andy
@DavidPacker Cóż, „wiedza o domenie” znajduje się w interfejsie. Interfejs mówi „domena potrzebuje tej funkcji”, a następnie magazyn danych zapewnia tę funkcjonalność. Jeśli masz IProductRepositoryinterfejs DoesNameExist, oczywiste jest, co powinna zrobić metoda. Ale upewnienie się, że Produkt nie może mieć takiej samej nazwy jak istniejący produkt, powinno być gdzieś w domenie.
Euforyczny
4

Musisz przeczytać Greg Young na temat sprawdzania poprawności zestawu .

Krótka odpowiedź: zanim zejdziesz zbyt daleko w gnieździe szczurów, musisz upewnić się, że rozumiesz wartość tego wymagania z perspektywy biznesowej. Jak naprawdę kosztowne jest wykrywanie i łagodzenie duplikacji, a nie zapobieganie jej?

Problem z wymogami „wyjątkowości” polega na tym, że bardzo często istnieje głębszy podstawowy powód, dla którego ludzie tego chcą - Yves Reynhout

Dłuższa odpowiedź: widziałem menu możliwości, ale wszystkie mają kompromisy.

Możesz sprawdzić duplikaty przed wysłaniem polecenia do domeny. Można to zrobić w kliencie lub w serwisie (twój przykład pokazuje technikę). Jeśli nie jesteś zadowolony z logiki wyciekającej z warstwy domeny, możesz osiągnąć ten sam wynik za pomocą DomainService.

class Product {
    void register(SKU sku, DuplicationService skuLookup) {
        if (skuLookup.isKnownSku(sku) {
            throw ProductWithSKUAlreadyExistsException(...)
        }
        ...
    }
}

Oczywiście zrobione w ten sposób implementacja usługi deduplikacji będzie musiała wiedzieć coś o tym, jak sprawdzić istniejący skus. A więc wpycha część pracy z powrotem do domeny, wciąż masz do czynienia z tymi samymi podstawowymi problemami (potrzebujesz odpowiedzi na sprawdzenie poprawności zestawu, problemy z warunkami wyścigu).

Możesz dokonać weryfikacji w samej warstwie trwałości. Relacyjne bazy danych są naprawdę dobre w sprawdzaniu poprawności zestawu. Nałóż ograniczenie unikatowości na kolumnę SKU Twojego produktu i możesz zacząć. Aplikacja po prostu zapisuje produkt w repozytorium, a gdy wystąpi problem, pojawi się bulgotanie naruszenia ograniczenia. Więc kod aplikacji wygląda dobrze, a twoja rasa została wyeliminowana, ale przeciekają reguły „domeny”.

Możesz utworzyć oddzielny agregat w swojej domenie, który reprezentuje zestaw znanych skusów. Mogę wymyślić tutaj dwie odmiany.

Jednym z nich jest katalog produktów; produkty istnieją gdzie indziej, ale związek między produktami i skusami jest utrzymywany przez katalog, który gwarantuje wyjątkowość SKU. Nie oznacza to, że produkty nie mają skus; skus są przypisywane przez ProductCatalog (jeśli potrzebujesz skus, aby był unikalny, osiągasz to poprzez posiadanie tylko jednego agregatu ProductCatalog). Przejrzyj wszechobecny język ze swoimi ekspertami w dziedzinie - jeśli coś takiego istnieje, może to być właściwe podejście.

Alternatywą jest coś więcej niż usługa rezerwacji SKU. Podstawowy mechanizm jest taki sam: agregat wie o wszystkich skusach, więc może zapobiec wprowadzeniu duplikatów. Ale mechanizm jest nieco inny: kupujesz dzierżawę SKU przed przypisaniem go do produktu; podczas tworzenia produktu przekazujesz dzierżawę do SKU. W grze nadal występują warunki rasowe (różne agregaty, a zatem różne transakcje), ale ma inny smak. Prawdziwym minusem jest to, że projektujesz w modelu domeny usługę leasingową bez prawdziwego uzasadnienia w języku domeny.

Możesz przyciągnąć wszystkie jednostki produktów do jednego agregatu - tzn. Opisanego powyżej katalogu produktów. Dzięki temu zyskujesz absolutnie wyjątkowość skusu, ale koszt to dodatkowe spory, modyfikowanie dowolnego produktu naprawdę oznacza modyfikację całego katalogu.

Nie podoba mi się potrzeba wyciągania wszystkich kodów SKU produktu z bazy danych, aby wykonać operację w pamięci.

Może nie musisz. Jeśli przetestujesz swój SKU z filtrem Bloom , możesz odkryć wiele unikalnych skusów bez ładowania zestawu.

Jeśli Twój przypadek użycia pozwala ci na dowolne ustalenie, który skus odrzucasz, możesz odeprzeć wszystkie fałszywe alarmy (nie jest to wielka sprawa, jeśli pozwolisz klientom przetestować skus, które proponują przed wysłaniem polecenia). Pozwoliłoby to uniknąć ładowania zestawu do pamięci.

(Jeśli chcesz być bardziej akceptowalny, możesz rozważyć leniwe ładowanie skus w przypadku dopasowania w filtrze kwitnienia; nadal ryzykujesz czasem załadowaniem całego skus do pamięci, ale nie powinno to być częstym przypadkiem, jeśli pozwolisz kod klienta, aby sprawdzić komendę pod kątem błędów przed wysłaniem).

VoiceOfUnreason
źródło
Nie podoba mi się potrzeba wyciągania wszystkich kodów SKU produktu z bazy danych, aby wykonać operację w pamięci. To oczywiste rozwiązanie, które zasugerowałem w pierwszym pytaniu i zakwestionowało jego działanie. Również IMO, polegające na ograniczeniu w twojej bazie danych, aby być odpowiedzialnym za wyjątkowość, jest złe. Jeśli przełączysz się na nowy silnik bazy danych i w jakiś sposób unikalne ograniczenie zostanie utracone podczas transformacji, zepsujesz kod, ponieważ informacji o bazie danych, które były tam wcześniej, już nie ma. W każdym razie dziękuję za link, ciekawa lektura.
Andy,
Być może filtr Blooma (patrz edycja).
VoiceOfUnreason