Afish napisał(a):
https://www.infoq.com/presentations/ddd-net-2
Od około 45 minuty gość pokazuje repozytoria. Opis twierdzi, że gość jest principalem w Microsofcie.
Czym jest ? ;)
Afish napisał(a):
https://www.infoq.com/presentations/ddd-net-2
Od około 45 minuty gość pokazuje repozytoria. Opis twierdzi, że gość jest principalem w Microsofcie.
Czym jest ? ;)
Co jest złego w Repository?
Zaczyna się od CustomerRepository z metodami GetCustomerById, GetCustomerByName, GetCustomerByNIP. Wszystko jest fajnie. Potem okazuje się, że trzeba pobrać klienta razem z fakturami, więc ktoś zmienia zawartość tych metod tak, że zachłannie pobierane są faktury. Oczywiście nigdzie w kodzie tego nie widać, do repozytorium nikt inny nie zagląda (bo po co?), ale ludzie dziwią się, że w wielu miejscach aplikacja zwolniła... Na szczęście znalezienie przyczyny nie trwa długo, i trzeba to naprawić. Więc co się robi? Nowe metody o więcej mówiących nazwach: GetCustomerWithInvoicesById, GetCustomerWithInvoicesByName, GetCustomerWithInvoicesByNIP. I jest wszystko dobrze!
Przynajmniej na razie, bo z czasem pojawia się potrzeba pobrania klientów wg adresu wraz z jego pracownikami, ale bez faktur: GetCustomersWithEmployeesButWithoutInvoicesByAddress, a potem potrzeba pobrania wszystkich klientów, z fakturami, bez pracowników, z ulubionymi filmami, bez kochanek wg grupy krwi żony i imienia kota córki prezesa: GetCustomersWithInvoicesWithoutEmployeesWithFavouriteMoviesWithoutMistressesByWifeBloodTypeAndChairmanDaughterCatName.
Im bardziej skomplikowana domena tym więcej potrzeb kombinacji warunków i wyciągania powiązanych encji, tym więcej metod w klasie Repozytorium. Kończymy z klasą z milionem metod... i to ma być dobre?
A jak masz listę faktur do wyświetlenia to jak to robisz? Założmy, że masz jakiegoś ORM i wtedy w tym kontrolerze bezpośrednio piszesz zapytanie? Jeśli nie to jak to u Ciebie wygląda?
@somekind: Mam kilka pytań.
A, i jeszcze jedno. Nie wiem, czy to Ty jesteś autorem tego artykułu (2016 rok): http://commitandrun.pl/2016/05/11/Repozytorium_najbardziej_niepotrzebny_wzorzec_projektowy/, ale tekst w sekcji 'Rozrost Repozytoriów' jest skopiowany z Twojego postu na początku tego wątku (2013 rok). ;)
Trzeźwy Kura napisał(a):
- Jak implementujesz operacje CRUDowe, jeśli nie masz repozytoriów, a kontrolery nie wiedzą nic o ORMie? Tworzysz klasy typu AddXService, UpdateXService itd., a w kontrolerach odwołujesz się do ich interfejsów tych klas?
- W jaki sposób budujesz zapytania i je egzekwujesz?
- Gdzie mapujesz encje na DTO?
- Gdzie jest walidacja danych wejściowych? Usługi zwracają jakieś enumy określające status operacji?
Zapytanie oraz mapowanie generuje algorytm. Ja tylko podaje do niego początkowe wartości jak restrykcja filtra i jego reguły oraz listę pól, które chcę pobrać. Algorytm filtrowania mam zamodelowany jako Domain Model, mapowanie oraz ORM chowam za abstrakcją DataQueryProvider. ReaderDtoApplicationService woła DataQueryProvider oraz Filtr z Domeny, który konfiguruje kryteria filtrowania i podaje je do Provaidera. StorageOperation implementuje w infrastrukturze a UnitOfWork steruje Contener IOC.
A algorytm mapowania, jak i wybierania zapytania SQL bazuje na zbieżności nazw pól pomiędzy PersistenceMode a DataReslut dla Generycznego DTO, które wzbogacam o metadane dla widoku.
Mam nadzieję, że pomogłem...
Trzeźwy Kura napisał(a):
@somekind: Mam kilka pytań.
- Jak implementujesz operacje CRUDowe, jeśli nie masz repozytoriów, a kontrolery nie wiedzą nic o ORMie? Tworzysz klasy typu AddXService, UpdateXService itd., a w kontrolerach odwołujesz się do ich interfejsów tych klas?
Staram się raczej nie używać sufiksu "Service". Wiele zależy od konkretnego przypadku, czasem np. Add/Update/Delete warto trzymać razem. No i nie używam interfejsów, bo one zwyczajnie nie mają sensu we własnym kodzie.
- W jaki sposób budujesz zapytania i je egzekwujesz?
Korzystając z API dostarczanego przez ORM. Nie wiem czy o to Ci chodziło w pytaniu, bo jest zbyt ogólne jak dla mnie.
- Gdzie mapujesz encje na DTO?
Jak najbliżej bazy danych. Przy wykorzystaniu z ORMa jest to miejsce wykonywania projekcji, czyli zazwyczaj metoda Select
.
- Gdzie jest walidacja danych wejściowych? Usługi zwracają jakieś enumy określające status operacji?
Różnie z tym jest, zazwyczaj wygląda to tak, że warstwa logiki aplikacji/biznesowej sprawdza sensowność podanych argumentów i rzuca wyjątki, gdy coś jest nie tak. Dokładniejsza walidacja wejścia odbywa się w warstwie prezentacji, gdzie sprawdzane są wszystkie reguły i do użytkownika zwracana jest lista komunikatów/kodów błędów.
A, i jeszcze jedno. Nie wiem, czy to Ty jesteś autorem tego artykułu (2016 rok): http://commitandrun.pl/2016/05/11/Repozytorium_najbardziej_niepotrzebny_wzorzec_projektowy/, ale tekst w sekcji 'Rozrost Repozytoriów' jest skopiowany z Twojego postu na początku tego wątku (2013 rok). ;)
Wielkie umysły myślą podobnie.
@somekind: Dziękuję, zaczyna mieć dla mnie sens to, co mówisz, ale mam jeszcze kilka pytań. Z tego, co zrozumiałem, w kontrolerach wykorzystuję usługi, które korzystają z pewnych data providerów, a gdy coś pójdzie nie tak, rzucany jest wyjątek, który przechwytuje kontroler, a następnie zwraca odpowiedni rezultat. Tylko zastanawia mnie, w czym taki data provider jest lepszy od "typowego" repozytorium (poza tym, że nie udaje czegoś, czym nie jest). W końcu wraz z rozwojem aplikacji metod będzie przybywać, więc trzeba te providery jakoś sensownie podzielić na mniejsze klasy. W pytaniu drugim chodziło mi o to, jak to zrobić, by było dobrze ;) Albo może tworzyć jakieś obiekty zapytań typu GetXByYQuery i przekazywać je do metod providerów? Btw świetnego masz tego bloga. ;)
Przykładowy pseudokod do dalszych rozważań:
public class CustomerController
{
private CustomerViewModelProvider provider;
private CustomerStore store;
// odpowiedni konstruktor
public DataPage<CustomerViewModel> Index(DataPageRequest<CustomerViewModel> request)
{
return provider.GetDataPage(request);
}
public CustomerDetailsViewModel Details(long customerId)
{
return provider.Get(customerId);
}
public CreationResult Create(CustomerCreateModel customer)
{
return store.Create(customer);
}
public DeleteResult Delete(long id)
{
return store.Delete(id);
}
public OperationResult WinThePrize(PrizeRequest request) // jest po 17 w piątek, idę na piwo i nie mam sensownego pomysłu
{
return customerService.Win(request);
}
}
public class CustomerService
{
public Win(PrizeRequest request)
{
// session to obiekt jakiegoś ORMa, niekoniecznie istniejącego
var customer = session.Get<Customer>(request.CustomerId);
var products = session.GetAll<Product>(request.ProductCategory, request.MinimumPrice);
var magicService = new DrawingService(products);
customer.Prize = magicService.GetPrize();
session.CommitAndFlush();
}
}
DataPageRequest
- generyczna klasa zawierająca takie pola jak numer strony do pobrania, liczba pozycji na stronę i warunki filtrowania (np. jako słownik nazwa właściwości: wartość filtra) oraz warunki sortowania (np. jako słownik nazw właściwości i kierunku sortowania).
DataResponse
- lista viewmodeli zmapowanych z bazy, numer strony, wielkość strony, liczba wszystkich stron.
Trzeźwy Kura napisał(a):
@somekind: Dziękuję, zaczyna mieć dla mnie sens to, co mówisz, ale mam jeszcze kilka pytań. Z tego, co zrozumiałem, w kontrolerach wykorzystuję usługi, które korzystają z pewnych data providerów, a gdy coś pójdzie nie tak, rzucany jest wyjątek, który przechwytuje kontroler, a następnie zwraca odpowiedni rezultat.
No ja bym raczej widział, że do pewnych rzeczy są serwisy, do pewnych providery, do jeszcze innych inne klasy. Nie ma po co tworzyć warstw wrapperów.
A wyjątki przechwytuje filtr albo handler, nie ma po co dręczyć tym kontrolery.
Tylko zastanawia mnie, w czym taki data provider jest lepszy od "typowego" repozytorium (poza tym, że nie udaje czegoś, czym nie jest). W końcu wraz z rozwojem aplikacji metod będzie przybywać, więc trzeba te providery jakoś sensownie podzielić na mniejsze klasy.
Ale kiedy niby będą przybywały metody?
Logika biznesowa nie korzysta z providerów, jedyne przypadki użycia mając wpływ na ich liczność, to nowy ekran w GUI.
Jeśli napiszesz ViewModelProvider niegenerycznie, to będą przybywały nowe klasy providerów.
Jeśli napiszesz generycznie, to nie będzie przybywać ani metod ani klas providerów.
W pytaniu drugim chodziło mi o to, jak to zrobić, by było dobrze ;) Albo może tworzyć jakieś obiekty zapytań typu GetXByYQuery i przekazywać je do metod providerów?
Też można - jeśli oczywiście masz jakieś "sztywne" ekrany w aplikacji, które nie pozwalają użytkownikowi na przeszukiwanie samodzielnie.
Nawiązując do wypowiedzi @somekind, chciałbym dodać, że...
Aby to wszystko było zgodne z SOC, powinieneś trzymać ViewModele w warstwie pośredniej pomiędzy logiką "biznesową, aplikacji" a warstwą prezentacji. Może to trochę przypominać pojęcie kontraktów z SOA albo prościej, możesz na to patrzeć jak na warstwę Common. Innym wyjściem jest zrobienie oddzielnej warstwy z ReaderViewModel (Osobiście mi się to nie podoba). Mógł byś również zdefiniować interface dla Readera w warstwie prezentacji ale to bez sensu.
Dalej jeszcze o SOC, jeśli w warstwie prezentacji nie renderujesz HTML i chcesz zwrócić zwykłe DTO - Resources (mniejsza o to jak to zwał) możesz sobie zrobić w warstwie prezentacji Dekorator na Readera, który doda jakieś dodatkowe elementy jak np. Linki na styl HATEOAS.
Chciałbym również zacząć pisać własnego bloga, w którym będę między innymi opisywał jak zrobić generyczną bibliotekę dla operacji CRUD. Chcielibyście coś takiego czytać...?
._. napisał(a):
Aby to wszystko było zgodne z SOC, powinieneś trzymać ViewModele w warstwie pośredniej pomiędzy logiką "biznesową, aplikacji" a warstwą prezentacji.
Można tak robić, i bardzo często ma to sens, ale nie trzeba. Kontrolery mogą np. składać viewmodele z danych otrzymanych z warstwy aplikacji.
Można tak robić, i bardzo często ma to sens, ale nie trzeba. Kontrolery mogą np. składać viewmodele z danych otrzymanych z warstwy aplikacji.
Zgadza się, ja tylko wskazuje na unikanie trzymania ViewModelów w warstwie aplikacji, wielu ludzi wpada w pułapkę albo tutaj, albo tam ;). A z tymi dekoratorami czy fasadą to też zależy. Moje zdanie jest takie, że jeśli chcesz dodać jakiegoś stringa na potrzeby View itp, to zrób to w kontrolerze. Jeśli do kontrolera wstrzykujesz kilka serwisów i do tego jeszcze jakiegoś utilsa i mieszasz tym wszystkim w kontrolerze, no to nie bardzo mi się to podoba. Z tego, co zauważyłem to ludzie też nadużywają AutoMappera gdzie większość ViewModeli zwrotnych można ustawiać przez konstruktor zamiast publiczne propertisy.
Krzywy Szczur napisał(a):
Ale żeby paginator mógł zadziałać to najpierw musi mieć te dane z repozytorium.
Kto tak powiedział? Repozytorium niczego nie przechowuje, więc nie może być źródłem niczego.
Tutaj chodzi tylko o podział tych danych, to nie jest prezentacja.
Jeśli coś wyświetlamy w GUI aplikacji to to jest prezentacja. A stronicowanie nie jest potrzebne w innych sytuacjach niż wyświetlanie danych użytkownikowi.
To nie są, żadne dane tylko metadane. Repository ma zwracać obiekty domenowe. Paginacja powinna być zainicjowana tylko raz, a potem tylko przetwarzać metadane, które jej podajesz przez Dto albo ViewModel. Paginacja w ogóle nie powinna być świadoma istnienia źródła "danych".
Zarejestruj się i dołącz do największej społeczności programistów w Polsce.
Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.
Shalomfajnego mosz bloga, dejwid
:D