Czy tworzyć serwisy, jeśli repozytorium pokrywa wszystkie akcje?

0

Cześć,
uczę się tworzyć aplikacje webowe (API w C#) - do tej pory tworzyłem tylko desktopy.

Powiedzmy, że mam AuthController z endpointami /login, /register i /refresh (dla tokenów JWT). Żeby nie umieszczać żadnej logiki w kontrolerze, stworzyłem AuthService z metodami Login(), Register() i Refresh(), które wstrzykuję za pomocą DI.

Ale co jeśli mam już stworzone repozytorium, które pokrywa wszystkie akcje kontrolera? Czy też powinienem tworzyć dla niego oddzielnie serwis?

Dla przykładu, chcę stworzyć CustomersController ze standardowymi metodami GET, POST, PUT i DELETE. Mam już utworzone CustomersRepository, które implementuje metody takie jak GetCustomers(), GetCustomerById(), AddCustomer(), itd. Metody te walidują model, ustawiają pola takie jak data dodania czy edycji, sprawdzają uprawnienia użytkownika (każdy użytkownik może mieć inne uprawnienia, więc pobieram je z bazy) i następnie wysyłają kwerendę do sql.

Czy w takim przypadku mogę do takiego kontrolera wstzyknąć samo repozytorium, czy może powinienem to jakoś rozbić na serwisy? (Nie chodzi mi o mikroserwisy, tworzę monolit)

Dodam jeszcze, że metody repozytorium nie zawierają całego kodu - walidację przeprowadzam w oddzielnej klasie, np. CustomersValidator, a uprawnienia sprawdzam z pomocą np. PrivilegesRepository.GetPrivilege().

Z góry bardzo dziękuję.

2

Jeśli nie masz potrzeby operowania w pamięci i sterowania przepływem to chyba jest ok.

Zawsze można później ten serwis dodać, jeśli zajdzie potrzeba.

Pragmatycznie 😉

0
rjakubowski napisał(a):

Jeśli nie masz potrzeby operowania w pamięci i sterowania przepływem to chyba jest ok.

Zawsze można później ten serwis dodać, jeśli zajdzie potrzeba.

Pragmatycznie 😉

Dziękuję :)

A kiedy by w zasadzie zaszła taka potrzeba, tzn. kiedy samo repozytorium nie wystarczy? Czytałem różne wpisy (chociażby Services vs. Repository - różnice), ale nie do końca rozumiem tego rozgraniczenia. No chyba, że repozytorium to klasa, która korzysta z bazy, a serwis to np. właśnie taki AuthService, który generuje tokeny JWT?

4
bbhzp napisał(a):

A kiedy by w zasadzie zaszła taka potrzeba, tzn. kiedy samo repozytorium nie wystarczy? Czytałem różne wpisy (chociażby Services vs. Repository - różnice), ale nie do końca rozumiem tego rozgraniczenia. No chyba, że repozytorium to klasa, która korzysta z bazy, a serwis to np. właśnie taki AuthService, który generuje tokeny JWT?

Jak dla mnie repozytorium, to miejsce gdzie robisz tylko operację na bazie danych. W teorii, kiedy chcesz zmienić źródło danych, to podmieniasz implementację i powinno działać. Sprawdzenie uprawnień powinno być wykonane w innym miejscu(np. już na etapie endpointu w middleware).

sprawdzają uprawnienia użytkownika (każdy użytkownik może mieć inne uprawnienia, więc pobieram je z bazy)

Uważam, że jest to złe miejsce. Jak zapisujesz dane na bazie, to niech repozytorium zajmuje się tylko zapisem na bazie, a nie sprawdzeniem uprawnień. To powinno się robić wyżej, middleware czy chodźby serwis.

4
bbhzp napisał(a):

Ale co jeśli mam już stworzone repozytorium, które pokrywa wszystkie akcje kontrolera?

To znaczy że albo Twój program w tym miejscu nie ma logiki i figuruje jedynie jako warstwa do zapisu danych (trochę tak jak program notatnik nie ma swojej logiki tylko zapisuje swój input jako plik); albo oznacza to że przez przypadek włożyłeś swoją logikę do repozytorium.

bbhzp napisał(a):

Czy też powinienem tworzyć dla niego oddzielnie serwis?

Nie ma nic złego w tym żeby kontroler brał od razu repozytorium (pod warunkiem że sam kontroler nie ma logiki).

Jak będziesz chciał potem dodać serwis to będzie to bardzo łatwe do dodania.

Niektórzy mogą Ci doradzać żeby dodać teraz ten serwis mimo że teraz nie jest potrzebny, bo może się okazać że będzie potrzebny później - tylko takie osoby wychodzą z założenia że "lepiej dodać teraz, bo potem będzie trudniej", ale IMO to jest całkowicie złe podejście. Nie ma powodu czemu dodanie tej zmiany powinno być trudne w przyszłości.

5
bbhzp napisał(a):

Ale co jeśli mam już stworzone repozytorium, które pokrywa wszystkie akcje kontrolera? Czy też powinienem tworzyć dla niego oddzielnie serwis?

Osobiście odradzam upychania repo do kontrolera. Uważam za dobrą praktykę ukrywania repozytorium za warstwą serwisu. Zwykle gdy program się rozrasta, to nagle okazuje się że serwis jest jednak potrzebny i albo robi się refactor albo zaczyna tworzyć kod spagetti.

3
Riddle napisał(a):

Nie ma nic złego w tym żeby kontroler brał od razu repozytorium (pod warunkiem że sam kontroler nie ma logiki).

Ja bym poszedł krok dalej i powiedział, że jeśli logiki nie ma zbyt dużo i/lub nie jest do końca jasne gdzie ją upakować to jest w porządku napisać ją w kontrolerze. Trzeba tylko być czujnym i gdy ta logika zacznie się rozrastać lub powtarzać w więcej niż 2 miejscach w aplikacji, trzeba ją ładnie wydzielić - czy to do funkcji, czy do serwisu.

Szumnie nazywa się to Vertical Slice Architecture, w praktyce chodzi o przestrzeganie YANGI i KISS. Jeśli twoja logika mieści się w jednym czy dwóch prostych ifach to zazwyczaj nie ma sensu robić kolejnej warstwy tylko dla niej.

2

A celujesz w konkretną architekturę / strukturę aplikacji, czy po prostu chcesz zrobić jak najprościej?

Jeśli celujesz w porty i adaptery albo inne heksagony (to są bardzo podobne podejścia), to kontroler i implementacja repozytorium są adapterami, więc poniekąd pomijasz implantację w core. Możesz ten serwis dodać dla picu, jeśli nie masz logiki której nie powinieneś wyciągnąć z repozytorium, a możesz przyjrzeć się implementacji i pomyśleć czy na pewno wszystko co w nim jest jest zależne np. Od SQLa albo Redisa, albo co tam masz.

Powiązanie kontrolera bezpośrednio z repozytorium może być ryzykowne z uwagi na to, że z jednej strony masz modele związane z wystawiamym API, z drugiej ze storage pod spodem. Łatwo tutaj przeholować z chodzeniem na skróty i zrobić "encja na twarz i pchasz". Jak raz w to wdepniesz, to ciężko się od tego uwolnić, za to kończysz z kiepskim API, lub kiepską schemą, lub jednym i drugim, a w dodatku jeśli pojawi się jakaś logika aplikacji (zależy co robisz) to może się okazać, że nie ma jej gdzie dodać, nie ma czasu refaktorować, więc dodasz dziekolwiek.

Możesz też założyć, że serwis ma konkretną odpowiedzialność i jedną publiczną metodę, choć dla mnie to już był overkill i na tym etapie odpuściłem, bo te odpowiedzialności musiałyby być dosyć rozbudowane by to się opłaciło.

1

@bbhzp: kiedy? Jak potrzebujesz logikę mieć jakąś na danych które skladujesz lub wyciągasz z bazy. Może jakiś side effect trzeba ogarnąć? Albo np. CSVke ciapnąć. Miliard możliwości.

3

Zależy. Dodanie serwisu, gdy będzie potrzebny (pojawi się dodatkowa logika) jest badzo proste. Z drugiej strony dodanie go od samego początku nie jest trudne i może być dobrym lekiem na to, że ktoś będzie leniwy i doda logikę w repozytorium zamiast utworzyć nowy serwis

Najważniejsze to mieć testy na poziomie aplikacji, dzięki czemu taki refaktor jest prosty

0

Dziękuję wszystkim za odpowiedzi :)

superdurszlak napisał(a):

A celujesz w konkretną architekturę / strukturę aplikacji, czy po prostu chcesz zrobić jak najprościej?

Tak po środku, czyli po prostu dobrze :) Nie korzystam na razie na sztywno z jakiś konkretnych architektur, ale nie daję też wszystkiego do funkcji main().

Faktycznie, gdybym musiał nagle dopisać jakąś logikę do endpointa, to najpierw stworzyłbym ją bezpośrednio przed wywołaniem metody z repozytorium w kontrolerze, zamiast pisać cały nowy serwis i przerzucać wszystko do niego (... a jest to złe podejście). O podobnym problemie piszą slsy, hzmzp i inni.

hawus napisał(a):

Ja bym poszedł krok dalej i powiedział, że jeśli logiki nie ma zbyt dużo i/lub nie jest do końca jasne gdzie ją upakować to jest w porządku napisać ją w kontrolerze. Trzeba tylko być czujnym i gdy ta logika zacznie się rozrastać lub powtarzać w więcej niż 2 miejscach w aplikacji, trzeba ją ładnie wydzielić - czy to do funkcji, czy do serwisu.

Szumnie nazywa się to Vertical Slice Architecture, w praktyce chodzi o przestrzeganie YANGI i KISS. Jeśli twoja logika mieści się w jednym czy dwóch prostych ifach to zazwyczaj nie ma sensu robić kolejnej warstwy tylko dla niej.

Też tak uważam; gdybym miał endpoint, który zwraca np. bieżącą godzinę, to zrobiłbym po prostu:

public IActionResult GetTime()
{
  return Ok(DateTime.Now);
}

zamiast tworzyć osobny TimeService, czy jakoś tak :)

rjakubowski napisał(a):

@bbhzp: kiedy? Jak potrzebujesz logikę mieć jakąś na danych które skladujesz lub wyciągasz z bazy. Może jakiś side effect trzeba ogarnąć? Albo np. CSVke ciapnąć. Miliard możliwości.

Prawda, no eksportu do CSV to bym w repozytorium nie umieścił, więc zostaje serwis :)

Michalk001 napisał(a):
bbhzp napisał(a):

sprawdzają uprawnienia użytkownika (każdy użytkownik może mieć inne uprawnienia, więc pobieram je z bazy)

Uważam, że jest to złe miejsce. Jak zapisujesz dane na bazie, to niech repozytorium zajmuje się tylko zapisem na bazie, a nie sprawdzeniem uprawnień. To powinno się robić wyżej, middleware czy chodźby serwis.

Masz rację, ale sprawdzając uprawnienia w samym repozytorium chciałem dać zabezpieczenie, aby ktoś inny, który nie pisze serwisu tylko bezpośrednio operuje na repozytoriach, nie wywołał przypadkiem niepożądanych zmian.

2
bbhzp napisał(a):

zamiast tworzyć osobny TimeService, czy jakoś tak :)

Hehe, to przed TimeProvider(chociaż nadal pewnie też), taką abstrakcję się tworzy, aby móc do testów robić moca czasu.

4
bbhzp napisał(a):
public IActionResult GetTime()
{
  return Ok(DateTime.Now);
}

Na tego typu uproszczeniach to dopiero można zrobić poślizg! A zwłaszcza przy testach.
Jeżeli masz takie przemyślenia to bym sugerował w ciemno robić te serwisy nawet jak wydają ci się nie potrzebne.

5
Riddle napisał(a):
bbhzp napisał(a):

Czy też powinienem tworzyć dla niego oddzielnie serwis?

Nie ma nic złego w tym żeby kontroler brał od razu repozytorium (pod warunkiem że sam kontroler nie ma logiki).

Pod warunkiem ze masz 100% pewnosc, ze cala reszta kodu bedzie funkcjonowala w identyczny sposob.
W rzeczywistosci bywa jednak bardzo "roznie" i wtedy w obliczu kazdej nowej sytuacji albo refaktorujesz wstecznie, albo tworzysz niejednolity, nieczytelny i rozmemlany strukturalnie kod.

Ustalenie od samego poczatku jedolitej struktury, np:

Controller(Responder(ActionService(Action(Repository))))

bardzo czesto na poczatku wydaje sie bezsensownym overkillem, jednak z czasem, w miare rozwoju applikacji, zaczynasz byc coraz bardziej sobie wdzieczny za utrzymanie dyscypliny przyjetej na starcie.

Riddle napisał(a):

Jak będziesz chciał potem dodać serwis to będzie to bardzo łatwe do dodania.

I robisz kociol w strukturze kodu, bo raz spinasz warstwe repo z kontrolerem, a innym razem z serwisem.
Pomijajac esetyke, zwieksza to np prog wejscia w kod.

Riddle napisał(a):

Niektórzy mogą Ci doradzać żeby dodać teraz ten serwis mimo że teraz nie jest potrzebny, bo może się okazać że będzie potrzebny później - tylko takie osoby wychodzą z założenia że "lepiej dodać teraz, bo potem będzie trudniej", ale IMO to jest całkowicie złe podejście. Nie ma powodu czemu dodanie tej zmiany powinno być trudne w przyszłości.

A niektorzy po prostu wychodza z zalozenia ze spojny strukturalnie kod jest o wiele latwiejszy w utrzymaniu, zwlasza, jesli w identyczny sposob tworzy sie nie jedna ale kilka aplikacji, ktore pozniej, w mniejszym lub wiekszym stopniu nalezy utrzymac.
W takim przypadku, zeby wejsc w kod takiej aplikacji, przynajmniej polowe wysilku poznawczego masz z glowy (kontekst strukturalny) i mozesz spokojnie skupic sie wylacznie na kontekscie biznesowym.

3

To raczej repozytorium jest zbędne niż serwis.
Zwłaszcza repozytorium, które nie jest repozytorium (czyli abstrakcją na warstwę danych zachowującą się jak kolekcja danych danego rodzaju).

3

A jeszcze jak taka tu dyskusja odnośnie serwisów i repo. @bbhzp poczytaj sobie o MediatR i zobacz, jak ułatwia życie.

1
rjakubowski napisał(a):

A jeszcze jak taka tu dyskusja odnośnie serwisów i repo. @bbhzp poczytaj sobie o MediatR i zobacz, jak ułatwia życie.

Potwierdzam, warto to stosować. Mamy pewien narzut rzeczy jakie musimy zrobić, ale w dłuższym okresie czasu łatwiej jest tym zarządzać. Plus nie potrzebujemy już pisać serwisów(brak serwisów, nie oznacza nie wydzielenia logiki to osobnych klas). Jak już tak polecamy, to też polecam language-ext(plus parę własnych metod rozszerzających)

3
hyper-stack napisał(a):

Ustalenie od samego poczatku jedolitej struktury, np:

Controller(Responder(ActionService(Action(Repository))))

...kończy się "repozytoriami", które mają 20 metod typu GetByID, FindByName, FindByAuthorID, FindByAuthorName, FindByTags, FindByMoonPhase itd. i przepychaniem tych samych danych przez 5 warstw.

1
hawus napisał(a):
hyper-stack napisał(a):

Ustalenie od samego poczatku jedolitej struktury, np:

Controller(Responder(ActionService(Action(Repository))))

...kończy się "repozytoriami", które mają 20 metod typu GetByID, FindByName, FindByAuthorID, FindByAuthorName, FindByTags, FindByMoonPhase itd. i przepychaniem tych samych danych przez 5 warstw.

Mylisz sie. W najgorszym wypadku konczy sie dziedziczeniem, a w optymalnym (php) traitem - oba z pojedynczymi zakresowymi metodami typu findBy(criteria), ale zeby miec jakiekolwiek pojecie o traitach trzeba mimo wszystko wyjsc ze znajomoscia php poza wersje 5.6 albo hejterskie shorty innych wylacznie prawilnych jezykow.

1
hyper-stack napisał(a):
hawus napisał(a):
hyper-stack napisał(a):

Ustalenie od samego poczatku jedolitej struktury, np:

Controller(Responder(ActionService(Action(Repository))))

...kończy się "repozytoriami", które mają 20 metod typu GetByID, FindByName, FindByAuthorID, FindByAuthorName, FindByTags, FindByMoonPhase itd. i przepychaniem tych samych danych przez 5 warstw.

Mylisz sie. W najgorszym wypadku konczy sie dziedziczeniem, a w optymalnym (php) traitem - oba z pojedynczymi zakresowymi metodami typu findBy(criteria), ale zeby miec jakiekolwiek pojecie o traitach trzeba mimo wszystko wyjsc ze znajomoscia php poza wersje 5.6 albo hejterskie shorty innych wylacznie prawilnych jezykow.

To działa tylko jak masz tylko jedną reprezentację, albo jak lubisz robić jeden model do reprezentowania wszystkich przypadków. No, albo jak lubisz re-implementować SQLa tymi criteriami. Ale czy nie łatwiej wtedy po prostu strzelić SQLem bezpośrednio do bazy?

2

Prawda, no eksportu do CSV to bym w repozytorium nie umieścił, więc zostaje serwis 😀

To jest przykład na coś innego co bym uważał - robienie wielkich i potężnych "serwisów". O ile Repozytoria (choć często tak naprawdę to DAO a nie repozytoria w znaczeniu DDDD) i Controllery to są obiekty które mają mieć pewne okreslene znaczenie jako warstwa infrastruktury, to uważam określenie typu "serwis" za bardzo niejednoznaczne i opisujące zbiór metod nie wiadomo co robiących. Na pewno eskport do CSV/pliku etc trzymałbym gdzies osobno jako jakiś inny byt

1
hawus napisał(a):

To działa tylko jak masz tylko jedną reprezentację,

Pojecia nie mam o czym Ty do mnie rozmawiasz.

hawus napisał(a):

albo jak lubisz robić jeden model do reprezentowania wszystkich przypadków.

NIe mam pojecia co w tym przypadku rozumiesz przez model. Jesli cos w rodzaju jakiegos uniwersalnego dto, encji albo dowolnego pojemnika, to z kolei Ty nie masz pojecia o czym ja mowie, natomiast jesli masz na mysli jakis uniwersalny flow postepowania, to tak, zgadza sie, lubie uogolniac przypadki tam gdzie to jest mozliwe.
Stad tez repozytorium jest dla mnie (i rowniez dla o wiele bardziej ogarnietych ode mnie) jedynie wrapperem wokol dowolnego data source klienta/manager'a, nie tylko bazodanowego.

hawus napisał(a):

No, albo jak lubisz re-implementować SQLa tymi criteriami.

Re-implementowac ..... SQL'a ...... criteria'mi ...... - nie jestem pewien czy dobrze rozumiem. Masz na mysli powtarzanie deklaracji kwerendy w kodzie? Jesli tak, to stanowczo nie mam tego na mysli. Kwerenda w sensie deklaracji jest jedna, a criteria ja po prostu dynamicznie adaptuja.

Ale czy nie łatwiej wtedy po prostu strzelić SQLem bezpośrednio do bazy?

A czymze jest repozytorium jesli nie giwera, ktora w taki czy inny sposob wlasnie do tej bazy (zreszta dowolnego data source) strzelasz?
Mozesz z wolnej reki (dbal) albo z podporka (orm), ale to jest caly czas repozytorium.

0
Aleksander-32 napisał(a):

Prawda, no eksportu do CSV to bym w repozytorium nie umieścił, więc zostaje serwis 😀

Aleksander-32 napisał(a):

Na pewno eskport do CSV/pliku etc trzymałbym gdzies osobno jako jakiś inny byt

W rozumieniu data source, czymze wg Ciebie rozni sie system plikow od bazy danych lub zewnetrznego api?

Aleksander-32 napisał(a):

to uważam określenie typu "serwis" za bardzo niejednoznaczne i opisujące zbiór metod nie wiadomo co robiących.

gerundium lub jak kto woli odsłownik jako integralny element nazwy serwisu, plus czytanie ze zrozumieniem definicji S z Solid (chociaz wlasciwie istnieja dwie interpretacje co wprowadza duzo zamieszania), plus (co wielu traktuje jako hardcore) zasada single public method. Te elementy skutecznie pozwalaja uniknac Twoich watpliwosci.

6

Olej serwisy. Już wyżej koledzy zasugerowali bibliotekę MediatR i CQRS powinien być Twoją nową drogą. W dobie takich bibliotek jak MediatR tworzenie serwisów aplikacyjnych nad warstwą repozytorium nie ma sensu.

Z biblioteką MediatR masz handler per use case, czyli CreateUserHandler, FindUserHandler, AuthenticateUserHandler. Handler przyjmuje w konstruktorze jako argument repozytorium czy inny provider do storage'u, albo jak używasz Entity Framework to DbContext, handler coś tam robi, wywołuje query i zwraca wynik.

Osobiście traktuję to podejście z wieloma handlerami jako czytelniejsze niż posiadanie jednej klasy UserService z 20 metodami łącznie na ponad 1k linii kodu."

Do tego bym ci polecił nie używanie modelu domenowego do odczytu danych tylko robienie zapytań/projekcji w handlerze i mapowanie od razu na DTO.

0
markone_dev napisał(a):

Osobiście traktuję to podejście z wieloma handlerami jako czytelniejsze niż posiadanie jednej klasy UserService z 20 metodami łącznie na ponad 1k linii kodu."

Dziękuję za odpowiedź, faktycznie czasem popełniałem takie klasy na 500 linii.... : )

Do tego bym ci polecił nie używanie modelu domenowego do odczytu danych tylko robienie zapytań/projekcji w handlerze i mapowanie od razu na DTO.

Masz rację, bo poleceń put, post i delete mam o wiele mniej i są one w miarę stałe, w przeciwieństwie do selectów, których mam tyle, ile jest różnych DTO : )

2
bbhzp napisał(a):

Dziękuję za odpowiedź, faktycznie czasem popełniałem takie klasy na 500 linii.... : )

To tyle co nic 😀

bbhzp napisał(a):

Masz rację, bo poleceń put, post i delete mam o wiele mniej i są one w miarę stałe, w przeciwieństwie do selectów, których mam tyle, ile jest różnych DTO : )

To była jedna z przyczyn dla których wymyślono CQRS a wcześniej CQS. W większości typowych aplikacji biznesowych zawsze będzie więcej odczytów niż zapisów i odczyty będą dużo bardziej złożone. A jak wiemy z teorii baz danych, model relacyjny sprawdza się doskonale przy zapisie, ale gdy przychodzi do odczytów, to im bardziej złożone zapytanie i im większy zbiór danych, tym bardziej trzeba się nagimnastykować i dobrze znać SQL-a, żeby go efektywnie napisać i wykonać. Jak na to nałoży się dodatkową abstrakcję w postaci ORM-a to już w ogóle papaj macha.jpg. Dlatego czasem w warstwie odczytowej rezygnuje się z ORM-a i pisze zapytania w SQL i używa Dappera, który nie daje takiego narzutu wydajnościowego jak choćby EF.

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.