w DDD, czy repozytoria powinny eksponować obiekty lub obiekty domeny?

11

Jak rozumiem, w DDD właściwe jest użycie wzorca repozytorium z zagregowanym katalogiem głównym. Moje pytanie brzmi: czy powinienem zwrócić dane jako obiekt lub domenę / DTO?

Może jakiś kod wyjaśni moje pytanie dalej:

Jednostka

public class Customer
{
  public Guid Id { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
}

Czy powinienem zrobić coś takiego?

public Customer GetCustomerByName(string name) { /*some code*/ }

Lub coś w tym stylu?

public class CustomerDTO
{
  public Guid Id { get; set; }
  public FullName { get; set; }
}

public CustomerDTO GetCustomerByName(string name) { /*some code*/ }

Dodatkowe pytanie:

  1. Czy w repozytorium powinienem zwrócić IQueryable czy IEnumerable?
  2. W usługi lub repozytorium, należy zrobić coś podobnego .. GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? lub po prostu stworzyć metodę podobną do GetCustomerBy(Func<string, bool> predicate)?
dorsz
źródło
Co GetCustomerByName('John Smith')zwróci, jeśli masz w bazie dwudziestu John Smiths? Wygląda na to, że zakładasz, że nie ma dwóch osób o tym samym imieniu.
bdsl

Odpowiedzi:

8

czy powinienem zwrócić dane jako byt lub obiekty domeny / DTO

Cóż, to całkowicie zależy od twoich przypadków użycia. Jedynym powodem, dla którego mogę pomyśleć o zwróceniu DTO zamiast pełnego bytu, jest to, że twój byt jest ogromny i wystarczy pracować nad jego podzbiorem.

W takim przypadku być może powinieneś ponownie rozważyć model domeny i podzielić duży podmiot na powiązane mniejsze podmioty.

  1. Czy w repozytorium powinienem zwrócić IQueryable czy IEnumerable?

Dobrą zasadą jest zawsze zwracanie możliwie najprostszego (najwyższego w hierarchii dziedziczenia) typu. Więc zwróć, IEnumerablechyba że chcesz pozwolić klientowi repozytorium pracować z IQueryable.

Osobiście uważam, że powrót IQueryablejest nieszczelną abstrakcją, ale spotkałem kilku programistów, którzy z pasją twierdzą, że tak nie jest. Moim zdaniem cała logika zapytań powinna być zawarta i ukryta przez Repozytorium. Jeśli pozwolisz kodowi wywołującemu dostosować zapytanie, jaki jest sens repozytorium?

  1. Czy w usłudze lub repozytorium powinienem zrobić coś takiego ... GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? lub po prostu stworzyć metodę podobną do GetCustomerBy (predykat Func)?

Z tego samego powodu, o którym wspomniałem w punkcie 1, zdecydowanie nie używaj GetCustomerBy(Func<string, bool> predicate). Z początku może się to wydawać kuszące, ale właśnie dlatego ludzie nauczyli się nienawidzić ogólnych repozytoriów. Jest nieszczelny.

Takie rzeczy GetByPredicate(Func<T, bool> predicate)są przydatne tylko wtedy, gdy są ukryte za konkretnymi klasami. Tak więc, gdybyś miał abstrakcyjną klasę bazową o nazwie RepositoryBase<T>odsłoniętej, protected T GetByPredicate(Func<T, bool> predicate)która była używana tylko przez konkretne repozytoria (np. public class CustomerRepository : RepositoryBase<Customer>), To byłoby w porządku.

MetaFight
źródło
Więc twoje powiedzenie, czy w porządku jest mieć klasy DTO w warstwie domenowej?
dorsz
Nie to chciałem powiedzieć. Nie jestem guru DDD, więc nie mogę powiedzieć, czy to dopuszczalne. Z ciekawości, dlaczego nie zwrócisz pełnego bytu? Czy to jest za duże?
MetaFight,
Och nie do końca. Chcę tylko wiedzieć, co jest właściwe i dopuszczalne. Czy ma to zwrócić pełny byt, czy tylko jego podzbiór. Myślę, że to zależy od twojej odpowiedzi.
codefish
6

Istnieje spora społeczność ludzi, którzy używają CQRS do wdrażania swoich domen. Mam wrażenie, że jeśli interfejs twojego repozytorium jest analogiczny do najlepszych praktyk przez nich stosowanych, to nie zbłądzisz zbyt daleko.

Na podstawie tego, co widziałem ...

1) Programy obsługi poleceń zwykle używają repozytorium do ładowania agregacji za pośrednictwem repozytorium. Polecenia są ukierunkowane na jedną konkretną instancję agregatu; repozytorium ładuje katalog główny według identyfikatora. Nie ma, jak widzę, przypadku, w którym polecenia są uruchamiane względem kolekcji agregatów (zamiast tego najpierw należy uruchomić zapytanie, aby uzyskać kolekcję agregatów, a następnie wyliczyć kolekcję i wydać polecenie każdemu z nich.

Dlatego w kontekstach, w których zamierzasz modyfikować agregację, oczekiwałbym, że repozytorium zwróci encję (inaczej root root agregatu).

2) Programy obsługi zapytań w ogóle nie dotykają agregatów; zamiast tego pracują z rzutami obiektów o wartościach agregujących, które opisują stan agregatu / agregatów w pewnym momencie. Pomyśl więc ProjectionDTO zamiast AggregateDTO, a masz dobry pomysł.

W kontekstach, w których zamierzasz uruchamiać zapytania względem agregatu, przygotowując je do wyświetlenia itd., Spodziewam się, że zostanie zwrócone DTO lub kolekcja DTO, a nie jednostka.

Wszystkie twoje getCustomerByPropertyrozmowy wyglądają dla mnie jak zapytania, więc należą do tej drugiej kategorii. Prawdopodobnie chciałbym użyć jednego punktu wejścia do wygenerowania kolekcji, więc chciałbym sprawdzić, czy

getCustomersThatSatisfy(Specification spec)

jest rozsądnym wyborem; procedury obsługi zapytań konstruują następnie odpowiednią specyfikację na podstawie podanych parametrów i przekazują tę specyfikację do repozytorium. Minusem jest to, że podpis naprawdę sugeruje, że repozytorium jest kolekcją w pamięci; nie jest dla mnie jasne, że predykat dużo kupuje, jeśli repozytorium jest jedynie abstrakcją uruchamiania instrukcji SQL względem relacyjnej bazy danych.

Istnieją jednak pewne wzorce, które mogą pomóc. Na przykład zamiast ręcznie budować specyfikację, przekaż do repozytorium opis ograniczeń i pozwól implementacji repozytorium zdecydować, co należy zrobić.

Ostrzeżenie: wykryto pisanie java

interface CustomerRepository {
    interface ConstraintBuilder {
        void setLastName();
        void setFirstName();
    }

    interface ConstraintDescriptor {
        void copyTo(ConstraintBuilder builder);
    }

    List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor);
}

SQLBackedCustomerRepository implements CustomerRepository {
    List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor) {
        WhereClauseBuilder builder = new WhereClauseBuilder();
        descriptor.copyTo(builder);
        Query q = createQuery(builder.build());
        //...
     }
}

CollectionBackedCustomerRepository implements CustomerRepository {
    List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor) {
        PredicateBuilder builder = new PredicateBuilder();
        descriptor.copyTo(builder);
        Predicate p = builder.build();
        // ...
}

class MatchLastName implements CustomerRepository.ConstraintDescriptor {
    private final lastName;
    // ...

    void copyTo(CustomerRepository.ConstraintBuilder builder) {
        builder.setLastName(this.lastName);
    }
}

Podsumowując: wybór między dostarczeniem agregatu a dostarczeniem DTO zależy od tego, czego oczekujesz od konsumenta. Sądzę, że będzie to jedna konkretna implementacja obsługująca interfejs dla każdego kontekstu.

VoiceOfUnreason
źródło
To wszystko dobrze i dobrze, ale pytający nie wspomina o użyciu CQRS. Masz rację, jego problem nie istniałby, gdyby to zrobił.
MetaFight,
Nie mam wiedzy na temat CQRS, bo o tym słyszałem. Rozumiem, kiedy zastanawiam się, co zwrócić, jeśli AggregateDTO lub ProjectionDTO, zwrócę ProjectionDTO. Potem getCustomersThatSatisfy(Specification spec)jest tylko lista właściwości, których potrzebowałem dla opcji wyszukiwania. Czy mam rację?
codefish
Niejasny. Nie wierzę, że AggregateDTO powinno istnieć. Celem agregacji jest upewnienie się, że wszystkie zmiany spełniają niezmienność biznesową. Jest to enkapsulacja reguł domeny, które muszą być spełnione - tzn. Zachowanie. Z drugiej strony prognozy są reprezentacjami niektórych migawek stanu, który był akceptowalny dla firmy. Zobacz edycję w celu wyjaśnienia specyfikacji.
VoiceOfUnreason
Zgadzam się z VoiceOfUnreason, że AggregateDTO nie powinno istnieć - myślę, że ProjectionDTO to model widoku tylko dla danych. Może dostosować swój kształt do wymagań dzwoniącego, w razie potrzeby pobierając dane z różnych źródeł. Korzeń zagregowany powinien być pełną reprezentacją wszystkich powiązanych danych, aby wszystkie reguły referencyjne mogły zostać przetestowane przed zapisaniem. Zmienia kształt tylko w bardziej drastycznych okolicznościach, takich jak zmiany tabeli DB lub zmodyfikowane relacje danych.
Brad Irby,
1

Na podstawie mojej wiedzy o DDD powinieneś to mieć,

public CustomerDTO GetCustomerByName(string name) { /*some code*/ }

Czy w repozytorium powinienem zwrócić IQueryable czy IEnumerable?

To zależy, w tym pytaniu znajdziesz różne poglądy różnych ludzi. Ale osobiście uważam, że jeśli twoje repozytorium będzie wykorzystywane przez usługę taką jak CustomerService, możesz użyć IQueryable, w przeciwnym razie trzymaj się IEnumerable.

Czy repozytoria powinny zwracać IQueryable?

Czy w usłudze lub repozytorium powinienem zrobić coś takiego ... GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? lub po prostu stworzyć metodę podobną do GetCustomerBy (predykat Func)?

W swoim repozytorium powinieneś mieć ogólną funkcję, jak powiedziałeś, GetCustomer(Func predicate)ale w swojej warstwie usług dodaj trzy różne metody, ponieważ istnieje prawdopodobieństwo, że twoja usługa zostanie wywołana z różnych klientów i potrzebują różnych DTO.

Możesz również użyć Ogólnego Wzorca Repozytorium dla wspólnej CRUD, jeśli jeszcze tego nie wiesz.

Muhammad Raja
źródło