Czy dodajecie jakąś dodatkową abstrakcję nad interfejsem Spring Data repository? Np. czy macie jakieś domenowe repository np. UserRepository i SpringDataJpaUserRepository, które je implementuje czy po prostu rozszerzacie UserRepository o JpaRepository? Dla mnie pierwsze podejście to lekka przesada, bo Spring sam nam dostarcza ten interfejs i jest mała szansa, że się kiedyś zmieni, więc preferuje drugie podejście
No oczywiście że tak - nie chcesz mieć silnego połączenia między Twoją logiką biznesową a warstwą persystencji.
Riddle napisał(a):
No oczywiście że tak - nie chcesz mieć silnego połączenia między Twoją logiką biznesową a warstwą persystencji.
Ale logika biznesowa powinna być encjach albo serwisach domenowych a repo zazwyczaj używa się w serwisach aplikacyjnych. To samo z transakcjami. Skoro mam jakieś @Transactional nad serwisem to chyba mogę mieć ten wstrzyknięty interfejs ze Spring Data JPA
Nofenak napisał(a):
Riddle napisał(a):
No oczywiście że tak - nie chcesz mieć silnego połączenia między Twoją logiką biznesową a warstwą persystencji.
Ale logika biznesowa powinna być encjach albo serwisach domenowych a repo zazwyczaj używa się w serwisach aplikacyjnych. To samo z transakcjami. Skoro mam jakieś @Transactional nad serwisem to chyba mogę mieć ten wstrzyknięty interfejs ze Spring Data JPA
A czemu miałbyś mieć adnotacje dot. persystencji w serwisie?
Zrobisz jak będziesz chciał, ale moim zdaniem to jest niepotrzebny coupling.
Rozumiem, że chodzi o coś w stylu:
interface CoupledUserRepository extends JpaRepository<User, Long> { }
interface UserRepository {
User findById(Long id);
}
class DecoupledUserRepository implements UserRepository {
private final CoupledUserRepository repository;
// ...
@Override
public User findById(Long id) {
return this.findById(id);
}
}
class UserService {
private final UserRepository repository;
// ...
public User findById(Long id) {
return this.findById(id);
}
}
Odpowiedź brzmi: raczej nie. Za każdym razem kiedy piszę taki kod zastanawiam się "czy to ten dzień gdy potrzebuję dodatkowej warstwy abstrakcji nad repozytorium springowym" i jeszcze nigdy nie padła odpowiedź "tak". Natomiast mam świadomość, że być może kiedyś ten dzień nadejdzie ;)
Tak jak piszesz - jeśli wiem, że na 99,99% zależność od JPA i Springa się nie zmieni (przez ostatnie 13 lat nie widziałem takiego przypadku) to po prostu nie widzę sensu w tworzeniu nadmiarowego kodu (trzy klasy zamiast jednej) oraz zaśmiecania kontekstu springowego (dwa obiekty zamiast jednego).
To jest bardziej delikatna sprawa, fakt że podchodzicie do tego tematu w stylu "JPA się nie zmieni, a więc mogę mieć silny-coupling na moją logikę" jest trochę amatorskie.
Może powinienem tutaj dodać, że utrzymanie odpowiedniego odseparowania persystencji od pozostałej części systemu ma bardzo dużo zalet, i tylko jedną z nich jest ochrona na to że biblioteka się zmieni. Faktycznych zalet jest bardzo dużo, i trochę szkoda że tak mało osób o nich myśli.
Podstawowym błędem tutaj jest założenie że logika odpowiedzialna za persystencję jest w całości i w 100% ogarniana przez JPA - takie uproszczenie. Podczas gdy bardzo rzadko jest tak w istocie; bo jeśli system się chociaż trochę rozrośnie to dochodzi do projektu nasza logika dotycząca persystencji - mapuje się jakąś wartość przed dodaniem, filtruje jakieś enumy, dodaje dodatkową walidację związaną z persystencją, umieszcza się wartość w polu o innym nazwie, mapuje się jakiś typ (bo np kiedyś pole było intem, teraz jest varcharem), i inne rzeczy. Błędem popełnianym tutaj przez programistów jest przekonanie że skoro ja napisałem tą logikę (i nie jest to cześć JPA) to to nie jest część persystencji, i muszę ją umieścić w serwisie. Co jest oczywiście niepoprawne - bo to nadal jest część persystencji, tylko nie ogarniana przez JPA - z tej uwagi powinna być w osobnej warstwie. Dla mnie to jest jasny sygnał że jeśli ktoś mówi takie coś:
wartek01 napisał(a):
Odpowiedź brzmi: raczej nie. Za każdym razem kiedy piszę taki kod zastanawiam się "czy to ten dzień gdy potrzebuję dodatkowej warstwy abstrakcji nad repozytorium springowym" i jeszcze nigdy nie padła odpowiedź "tak".
To są dwa wyjścia: albo faktycznie piszesz programy w których 100% logiki persystencji jest ogarniane przez JPA i nie masz żadnych decyzji programowych na jakąkolwiek manipulacje persystencją z Twojego kodu (czego jeszcze nie widziałem), albo po prostu nie zdajesz sobie sprawy z efektu który opisałem.
Łatwo jest pomylić te dwie rzeczy jeśli nie myślimy o loose-couplingu i stawiamy tylko takie uproszczenia w stylu: "mój kod - to serwis, jpa kod to persystencja".
Jednym z głównych powodów czemu aplikacje zamieniają się w legacy to jest (oprócz braku testów) niepoprawne odseparowanie elementów - potem jest narzekanie: "ale się z tego zrobił monolit" albo "ale z tego jest spaghetii" albo "ale to jest nieutrzymywalne"; i łatwo jest zapomnieć wtedy że to się w dużej mierze właśnie bierze z takiej niechęci do dodania dodatkowej warstwy abstrakcji bo nie ma po co - tymczasem jest po co.
Riddle napisał(a):
To jest bardziej delikatna sprawa, fakt że podchodzicie do tego tematu w stylu "JPA się nie zmieni, a więc mogę mieć silny-coupling na moją logikę" jest trochę amatorskie.
Powiedziałbym, że overengineering jest amatorski. Tworzenie czegoś, co ma w zamyśle rozwiązać problemy, które nie zaistnieją jest średnią praktyką.
Podstawowym błędem tutaj jest założenie że logika odpowiedzialna za persystencję jest w całości i w 100% ogarniana przez JPA - takie uproszczenie. Podczas gdy bardzo rzadko jest tak w istocie; bo jeśli system się chociaż trochę rozrośnie to dochodzi do projektu nasza logika dotycząca persystencji - mapuje się jakąś wartość przed dodaniem, filtruje jakieś enumy, dodaje dodatkową walidację związaną z persystencją, umieszcza się wartość w polu o innym nazwie, mapuje się jakiś typ (bo np kiedyś pole było intem, teraz jest varcharem), i inne rzeczy.
To wtedy zamieniasz interface JpaRepository
na jakieś class SimpleJpaRepository
(czy coś podobnego bo niektórzy piszą własne generyczne implementacje) i dopisuejsz własny kod do warstwy repozytorium. Cyk, pora na CSa.
To są dwa wyjścia: albo faktycznie piszesz programy w których 100% logiki persystencji jest ogarniane przez JPA i nie masz żadnych decyzji programowych na jakąkolwiek manipulacje persystencją z Twojego kodu (czego jeszcze nie widziałem), albo po prostu nie zdajesz sobie sprawy z efektu który opisałem.
Może nie widziałeś, ja regularnie przeglądam repozytoria ludzi w dosyć dużym banku i dosyć często widuję tego typu rzeczy gdzie JPA załatwia wszystko. Ba, powiedziałbym, że Spring Data JPA tak świetnie wszystko ogarnia, że co poniektórzy używają encji jako DTO, ale to już inna historia.
Łatwo jest pomylić te dwie rzeczy jeśli nie myślimy o loose-couplingu i stawiamy tylko takie uproszczenia w stylu: "mój kod - to serwis, jpa kod to persystencja".
W tej chwili to uproszczenie postawiłeś tylko i wyłącznie ty. Ogólnie to cały twój post to seria niezrozumiałych dla mnie uproszczeń na zasadzie: "nie zgadzasz się ze mną więc uważasz, że twój kod to serwis a jpa to persystencja".
Tak, jak najbardziej dodaję. Jest wiele zalet repozytorium domenowego, oprócz posiadania repozytorium JPA. Jedna z prostszych, to chociażby agregacja "encjowych" repozytorium. Przecież poza JPA, można mieć chociażby JDBC repozytorium. Albo moja aplikacja używa dwóch lub więcej baz danych. Wtedy taki kod wyglądałby tak:
@Component
class DomainUserRepository(
private val jdbcUserRepository: JdbcUserRepository,
private val jpaUserRepository: JpaUserRepository,
private val oracleUserRepository: OracleUserRepository
) : UserRepository {
Poza tym, przy użyciu repozytorium domenowego możesz mieć zupełnie różne reprezentacje danych. Serwisy lub inne miejsca z logiką biznesową wstrzykują sobie tylko UserRepository
, jeżeli będzie podmiana:
- bazy danych
- ORMa
- czegokolwiek innego, nawet na np. cache
Interfejs zostaje taki sam.
Według mnie wysiłek w dodaniu warstwy abstrakcji jest żaden.
wartek01 napisał(a):
Riddle napisał(a):
To jest bardziej delikatna sprawa, fakt że podchodzicie do tego tematu w stylu "JPA się nie zmieni, a więc mogę mieć silny-coupling na moją logikę" jest trochę amatorskie.
Powiedziałbym, że overengineering jest amatorski. Tworzenie czegoś, co ma w zamyśle rozwiązać problemy, które nie zaistnieją jest średnią praktyką.
Owszem, ale świat nie dzieli się na programy tightly-coupled oraz przeinżynierowane.
Dobra aplikacja powinna zarówno stosować YAGNI, zarówno nie być przeinżynierowana, nie powinna mieć niepotrzebnych elementów, ale zarówno też nie powinna być mocno powiązana. Musisz spełnić każdy z tych elementów żeby wytworzyć dobrą apkę. Apka która nie jest przeinżynierowana, ale silnie powiązana nie będzie tak dobra, jak taka które jest prosta/YAGNI ale jest loosely-coupled.
Złotym środkiem jest tutaj:
- Brak overenineeringu
- YAGNI
- loose-coupling
Jeśli pomijasz któryś z tych elementów, to raczej robisz coś źle.
Podstawowym błędem tutaj jest założenie że logika odpowiedzialna za persystencję jest w całości i w 100% ogarniana przez JPA - takie uproszczenie. Podczas gdy bardzo rzadko jest tak w istocie; bo jeśli system się chociaż trochę rozrośnie to dochodzi do projektu nasza logika dotycząca persystencji - mapuje się jakąś wartość przed dodaniem, filtruje jakieś enumy, dodaje dodatkową walidację związaną z persystencją, umieszcza się wartość w polu o innym nazwie, mapuje się jakiś typ (bo np kiedyś pole było intem, teraz jest varcharem), i inne rzeczy.
To wtedy zamieniasz
interface JpaRepository
na jakieśclass SimpleJpaRepository
(czy coś podobnego bo niektórzy piszą własne generyczne implementacje) i dopisuejsz własny kod do warstwy repozytorium. Cyk, pora na CSa.
No i dokładnie tak się powinno robić.
To są dwa wyjścia: albo faktycznie piszesz programy w których 100% logiki persystencji jest ogarniane przez JPA i nie masz żadnych decyzji programowych na jakąkolwiek manipulacje persystencją z Twojego kodu (czego jeszcze nie widziałem), albo po prostu nie zdajesz sobie sprawy z efektu który opisałem.
Może nie widziałeś, ja regularnie przeglądam repozytoria ludzi w dosyć dużym banku i dosyć często widuję tego typu rzeczy gdzie JPA załatwia wszystko. Ba, powiedziałbym, że Spring Data JPA tak świetnie wszystko ogarnia, że co poniektórzy używają encji jako DTO, ale to już inna historia.
No dlatego mówię:
Są dwa wyjścia:
- Albo faktycznie Spring Data JPA w Twoich projektach faktycznie świetnie wszystko ogarnia że 100% persystencji jest załatwione w nich
- Albo coś Ci umyka, i część persystencji przemyka do warstw logiki.
Może być bardzo prawdopodobne że u Ciebie jest ten pierwszy przypadek, i wtedy wszystko jest w porządku. Ale ja nie popadałbym w taką zuchwałość żeby od razu wykluczyć to drugie.
Łatwo jest pomylić te dwie rzeczy jeśli nie myślimy o loose-couplingu i stawiamy tylko takie uproszczenia w stylu: "mój kod - to serwis, jpa kod to persystencja".
W tej chwili to uproszczenie postawiłeś tylko i wyłącznie ty. Ogólnie to cały twój post to seria niezrozumiałych dla mnie uproszczeń na zasadzie: "nie zgadzasz się ze mną więc uważasz, że twój kod to serwis a jpa to persystencja".
To co próbuję powiedzieć to to, że tight-coupling to jest bardzo poważny problem i źródło wielu problemów w dużych aplikacjach, i spora część tego bierze się z niezrozumienia kilku czynników.
===
Myślę że się zgadzamy pod tymi względami:
- Jeśli faktycznie 100% logiki persystencji jest ogarniane przez JPA, to nie ma potrzeby na dodatkową warstwę
- Ale jeśli jakaś logika odn. persystencji musi zostać napisana przez nas, wtedy trzeba dodać taką warstwę żeby uniknąć thigh-coupling.
Także chyba tutaj mamy zgodę i nie ma nieporozumień.
===
Może jeszcze tytułem dygresji, powiem że nie specjalnie zgadzam się z tym że może istnieć coś takiego jak "przeinżynierowanie". No bo - rozumiem skąd się to bierze, otwieramy projekt który wiemy że jest prosty, i nagle tam jest 100 klas, 100 fabryk, 100 adapterów, controlery, mapowania, nie wiadomo co jeszcze, a to jest przecież prosty problem - dodawanie wzorców na siłę, instalowanie bibliotek żeby oszczędzić linijkę kodu. Mamy wtedy ochotę powiedzieć że to jest "przeinżynierowane". Ale moim zdaniem to nie jest odpowiedni termin, taki projekt po prostu łamie YAGNI - dodaje niepotrzebne rzeczy, dodaje kod który nie jest wymagany i tylko komplikuje rzecz. Wtedy określenia "łamanie YAGNI" oraz "przeinżynierowanie" są synonimami.
Nie byłoby nic złego w takim synonimie, gdyby nie to że słowo "przeinżynierowanie" ma negatywny skutek. Mianowicie taki jak teraz mamy w tej rozmowie - chcemy dodać coś co faktycznie jest pomocne/potrzebne/zwiększające jakość (jakiś wzorzec, jakaś enkapsulacja, jakaś abstrakcja) - ale nie chcemy go dodać tylko i wyłącznie dlatego żeby nie zostać posądzonym o overengineering.
Podsumowując, dla mnie określenie "overengineering" nie jest specjalnie mądre, to jak powiedzenie "jesz zbyt zdrowe jedzenie" albo "masz byt proste koszule w szafce" albo "Twój egzamin jest zbyt dobrze zdany" albo "masz zbyt wysokie oceny". To co na prawdę nas irytuje w aplikacjach to jak ktoś wrzuca niepotrzebny shit - to jest główny problem, czyli łamanie YAGNI.
@Riddle: Tylko, że w springu jeśli dotykasz encji to jednocześnie dotykasz warstawy persystencji w 99% przypadków (pozdrowienia dla dynamic proxy). I teraz jeśli wykonujesz jakiekolwiek operacje w tranzakcji to niejawnie też tego dotykasz i nie musisz tu wcale wstrzyknietego repozytorium czy wykonywać np .save().