Dodatkowa abstrakcja nad Spring Data repository

0

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

2

No oczywiście że tak - nie chcesz mieć silnego połączenia między Twoją logiką biznesową a warstwą persystencji.

0
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

1
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.

0

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).

2

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.

5
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".

0

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.

0
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.

1

@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().

1
Schadoow napisał(a):

@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().

Tak, to jest dodatkowa komplikacja którą trzeba wziąć pod uwagę podczas kminienia silnego powiązania z frameworkiem.

To dotykanie o którym mówisz (jednocześnie dotykasz warstawy persystencji), to jak rozumiem jest runtime-dependency, tzn. w compile-time nie widać czy operacja na encji dotknie persystencji czy nie - dzieje się to w runtime'ie dopiero. Jeśli tak, to nie jest problem i tak może być; według mnie przynajmniej.

W skrócie, jak można wiedzieć czy połączenie jest luźne czy silne - jeśli mogę wymienić całą persystencję Spring JPA, na jakąś moją customową, i nie muszę nic zmienić w logice - wtedy jest luźne. Czy to jest zasadne żeby takie coś wprowadzać - moim zdaniem im większy projekt, im więcej ma zmian, i im dłużej development nad nim trwa, tym bardziej ma to sens żeby oddzielić te rzeczy całkowicie.

2

@Riddle: tylko wiesz, że wtedy walczysz w frameworkiem i konczy się to konstrukcjami potworkami albo pierdyliard maperów tylko po to oddzielić warstwy ?

Poza tym w dużych projektach RDBMS to nie jest tylko "warstwa persystencj". W twoim rozumieniu RDBMS trzeba by traktować jako zewnętrzny serwis które ma swoje api i obsługe błędów którą musisz obsłużyć i dopiero w środku RDBMS'a odbywa sie persystencja. I w dużych projektach nie wymienisz RDBMS'a jednego na drugiego od tak.

Generalnie jak masz taką potrzebę to nie używasz springa ¯_(ツ)_/¯

0
Schadoow napisał(a):

@Riddle: tylko wiesz, że wtedy walczysz w frameworkiem i konczy się to konstrukcjami potworkami albo pierdyliard maperów tylko po to oddzielić warstwy ?

Generalnie jak masz taką potrzebę to nie używasz springa ¯_(ツ)_/¯

Czemu niby miałbym nie móc? Nie chcę pisać swojego MVC i dostępu do bazy więc użyję sobie spring-mvc albo spring-jpa, tylko podchodzę do tego jak do każdej innej biblioteki. Do spring-mvc potrzebuję wstrzyknąć zależności więc użyję DI; ale to nie znaczy że oprócz tego moje pozostałe moduły mają cokolwiek wiedzieć o nim.

Schadoow napisał(a):

Poza tym w dużych projektach RDBMS to nie jest tylko "warstwa persystencj". W twoim rozumieniu RDBMS trzeba by traktować jako zewnętrzny serwis które ma swoje api i obsługe błędów którą musisz obsłużyć i dopiero w środku RDBMS'a odbywa sie persystencja. I w dużych projektach nie wymienisz RDBMS'a jednego na drugiego od tak.

No nie no, wtedy to jest trochę co innego i inaczej trzeba do tego podejść.

0

Wolę mieć o jedną warstwę abstrakcji za dużo niż za malo.

Nie widzę powodu dlaczego miałbym w domenie nasrać importami ze springa. Do takiego projektu dołączyłem, tak staram się pisać i development aplikacji serio sprawia mi przyjemność. Na początku miałem wrażenie, że było trochę overengeneringu (i być może faktycznie coś się go znajdzie) ale powtórzę raz jeszcze: wolę mieć jedną warstwę abstrakcji za dużo niż za mało.

1
Riddle napisał(a):
  • Brak overenineeringu
  • YAGNI
  • loose-coupling

Więc po kolei:

  1. Nie będzie odejścia z Springa, tak po prostu. Nie mam statystyk, ale jeszcze nigdy nie widziałem migracji aplikacji RESTowej z Springa na coś innego, więc IMO takie sytuacje to wyjątki.
  2. Loose-coupling ma sens wtedy i tylko wtedy, gdy ma sens. To, że nie będziesz miał jakichś dziwnych zależności pomiędzy warstwami pakowanymi do jednego JARa, które poza ten JAR nie wyjdą - nie daje ci absolutnie nic.
  3. Więc tak, wrzucanie jeszcze jednej warstwy abstrakcji do aplikacji, tylko po to, żeby rozwiązać potencjalny - ale bardzo nieprawdopodobny - scenariusz to dla mnie klasyczny overengineering.

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.

Może i tak, natomiast nie rozmawiamy tutaj o problemach w dużych aplikacjach, a problemach w aplikacjach springowych. Nawet jeśli aplikacja springowa jest duża to zbiór "duże aplikacje springowe" nie jest tożsamy z zbiorem "duże aplikacje".

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.

Bo jedno i drugie to są dwie strony tej samej monety.
Przy czym w moim odczuciu jakoś się tak utarło, że YAGNI odnosi się do wymyślania sobie scenariuszy, a "overengineering" to rozwiązywanie problemów, które faktycznie są na stole.

1
wartek01 napisał(a):
  1. Nie będzie odejścia z Springa, tak po prostu. Nie mam statystyk, ale jeszcze nigdy nie widziałem migracji aplikacji RESTowej z Springa na coś innego, więc IMO takie sytuacje to wyjątki.

Fakt że wydaje Ci się że odejście od frameworka to jedyna zaleta loose-couplingu nie przemawia za wiarygodnością tego co mówisz.

  1. Loose-coupling ma sens wtedy i tylko wtedy, gdy ma sens. To, że nie będziesz miał jakichś dziwnych zależności pomiędzy warstwami pakowanymi do jednego JARa, które poza ten JAR nie wyjdą - nie daje ci absolutnie nic.

Czyli to co mówisz, to to że ponieważ wynikowo spring i Twoja logika biznesowa lądują ostatecznie w jednym JAR'ze, to znaczy że to jest okej żeby mieć silne powiązania pomiędzy frameworkiem i logiką biznesową?

  1. Więc tak, wrzucanie jeszcze jednej warstwy abstrakcji do aplikacji, tylko po to, żeby rozwiązać potencjalny - ale bardzo nieprawdopodobny - scenariusz to dla mnie klasyczny overengineering.

Nie wiem po co próbujesz to nazwać czymś innym niż jest. Rozwiązywanie potencjalnych problemów, ale nieprawdopodobnych (i takich których jeszcze nie musisz rozwiązać) to jest łamanie YAGNI, po prostu.

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.

Bo jedno i drugie to są dwie strony tej samej monety.
Przy czym w moim odczuciu jakoś się tak utarło, że YAGNI odnosi się do wymyślania sobie scenariuszy, a "overengineering" to rozwiązywanie problemów, które faktycznie są na stole.

No to się źle utarło - to pewnie jest też dodatkowe wyjaśnienie czemu masz takie podejście. Od dziesiątek lat programiści piszą kod który jest niepotrzebny, ten efekt jest znany od bardzo dawna, i zasada która o tym mówi to jest YAGNI. Za każdym razem kiedy piszesz coś co nie jest wymagane przez klienta, albo rozwiązujesz problem którego nie musisz jeszcze rozwiązywać łamiesz YAGNI, po prostu.

===

Nie chciałem być tag dobitny, ale chyba muszę: @wartek01 To że nie rozumiesz zasadności jakiejś praktyki, nie znaczy od razu że to jest niepotrzebne. Bo to się właśnie dzieje - polecam dodać loose-coupling na Twoją aplikacje, Ty uważasz że to jest niepotrzebne, a więc zarzucasz overengineering - ale umykają Ci wszystkie zalety tego podejścia o których nie pomyślałeś.

1

Kiedyś w aplikacjach, dziś dumnie określanych jako legacy, też nie stosowano się do reguł/dobrych praktyk (jedne ignorowano celowo, inne np nie były jeszcze wtedy sformalizowane) za to stosowano takie względem ktorych nie wiedziano że się zemszczą i drastycznie utrudnia późniejsze utrzymanie (klasyczny crud warstwowy z logiką wymieszaną po encjach i serwisach w całej aplikacji bez żadnej enkapsulacji i testów).

Ignorancja i nieumiejętność nauki na cudzych błędów, problem z autorytetami i konsekwentne trzymanie się swojego wyuczonego punktu widzenia bez refleksji, że może ktoś jednak ma rację zmierza imho tylko do powtórki z rozrywki i powstawania kolejnych potworków w których w domenie (jeśli takowa w ogóle istnieje) na prawo i lewo latają min. Springowy repozytoria ;)

5

TLDR;
Repozytorium ma sens tylko w przypadku stosowania DDD i persystencji agregatow. W CRUD-ach i tego typu apkach to przerost formy nad treścią, a zmiana ORM-a to i tak więcej pracy niż napisanie nowej implementacji UserRepository i podmiana w kontenerze Dependency Injection.

Kiedyś też robiłem dodatkowe abstrakcje (repository, unit of work) nad Entity Frameworkowym DbContextem w praktycznie każdym projekcie. Bo loose coupling, bo łatwo mogę zmienić sobie ORM-a bazę, itd. Pewnego razu robiliśmy projekt, dość duża aplikacja składająca się z backendu wystawiającego API (modularny monolit) dla aplikacji klienckich web i mobile. Pod spodem moduły działały sobie na Entity Framework Core. Oczywiście było generyczne repo żeby ukryć ten szczegół implementacyjny, unit of work, specification pattern, itd. Po pewnym czasie (wzrost ruchu i skomplikowanie złożoności zapytań) okazało się, że w jednym z modułów wąskim gardłem jest Entity Framework i jego LINQ, czyli język wyrażeń lambda, tłumaczony na SQL. Zapytania w LINQu stawały się co raz bardziej nieczytelne a wynikowy SQL nieoptymalny. Rozwiązania były dwa: kontynuować używanie EF Core wraz z normalnymi zapytaniami SQL zamiast LINQ lub zmiana ORM-a. Stwierdziliśmy, że spróbujemy zmienić ORM na Dappera. W końcu co może pójść źle, mamy repozytoria UOW oraz specification pattern. Okazało się, że to nie takie proste ponieważ model dziedzinowy, który był mapowany na encje w bazie danych był niejako zależny od użytego ORM-a - konkretnie asocjacje i zależności pomiędzy encjami domenowymi. EF Core, ale też NHibernate czy jakikolwiek inny ORM wymusza niejako wygląd modelu dziedzinowego, choćby w postaci Navigation Property czy Reference, a to czego potrzebowaliśmy to bardziej płaski i anemiczny Read Model aniżeli graf obiektów. Aha, argumentu o dwóch modelach (persystencji i dziedziny) też nie kupuję.

Suma sumarum aby zmienić ORM-a mimo posiadania tych wszystkich wspaniałych abstrakcji w warstwie persystencji musieliśmy zmienić również model domenowy, co pociągnęło za sobą zmiany w serwisach aplikacyjnych. Generalnie cały moduł został zrefaktoryzowany. To co nam uratowało dupę to nie abstrakcja nad Entity Framework Core tylko odpowiednia modułowa architektura, gdzie każdy moduł miał swoje warstwy (application, domain, persistence) a moduły komunikowały się asynchronicznie poprzez zdarzenia i komendy. A na poziomie architektury modułu zrobiliśmy prosty CQRS. Zapisy szły sobie dalej przez EF Core a odczyty Dapperem i mapowane od razu na DTO, czyli z pominięciem EF-a.

Dlatego panowie i panie dbajcie w swoją architekturę tak, aby w razie W mogliście łatwo przepisać moduł/serwis bez konieczności dotykania innych modułów/serwisów.

1
Riddle napisał(a):

Fakt że wydaje Ci się że odejście od frameworka to jedyna zaleta loose-couplingu nie przemawia za wiarygodnością tego co mówisz.

Nie rozmawiamy o loose-couplingu jako takim, tylko o bardzo specyficznym przypadku loose-couplingu - tj. dorzuceniu warstwy abstrakcyjnej na repozytorium w aplikacji springowej.

Czyli to co mówisz, to to że ponieważ wynikowo spring i Twoja logika biznesowa lądują ostatecznie w jednym JAR'ze, to znaczy że to jest okej żeby mieć silne powiązania pomiędzy frameworkiem i logiką biznesową?

Mówię, że stworzenie dodatkowej warstwy abstrakcji w kontekście pytania zadanego przez OPa nie ma sensu w większości przypadków, więc nie widzę sensu jej wprowadzenia.

Nie wiem po co próbujesz to nazwać czymś innym niż jest. Rozwiązywanie potencjalnych problemów, ale nieprawdopodobnych (i takich których jeszcze nie musisz rozwiązać) to jest łamanie YAGNI, po prostu.

"overengineering" to termin często wykorzystywany w rozmowach i w dodatku starszy niż zwrot YAGNI, więc gdybym bawił się w purystę to bym tobie powiedział, żebyś to ty przestał używać zwrotu YAGNI.

No to się źle utarło - to pewnie jest też dodatkowe wyjaśnienie czemu masz takie podejście. Od dziesiątek lat programiści piszą kod który jest niepotrzebny, ten efekt jest znany od bardzo dawna, i zasada która o tym mówi to jest YAGNI. Za każdym razem kiedy piszesz coś co nie jest wymagane przez klienta, albo rozwiązujesz problem którego nie musisz jeszcze rozwiązywać łamiesz YAGNI, po prostu.

To twoja opinia, natomiast nie oznacza, że jest ona jakoś słuszna. Język i terminologia to żywy twór i ciągle ewoluuje, a zwrot "YAGNI" i "overengineering" nie są pojęciami technicznymi (tj. nie mają ściśle określonych desygnatów).

Nie chciałem być tag dobitny, ale chyba muszę: @wartek01 To że nie rozumiesz zasadności jakiejś praktyki, nie znaczy od razu że to jest niepotrzebne. Bo to się właśnie dzieje - polecam dodać loose-coupling na Twoją aplikacje, Ty uważasz że to jest niepotrzebne, a więc zarzucasz overengineering - ale umykają Ci wszystkie zalety tego podejścia o których nie pomyślałeś.

Bardzo mi się podoba, jak bardzo starasz się zmienić temat - tj. nie chcesz za bardzo rozmawiać na temat "co da nam dodatkowa abstrakcja na repozytorium springowe" tylko "jakie są zalety i wady loose-couplingu". Wbrew temu, co próbujesz sobie samemu wytłumaczyć - nie walczę z LC jako takim, ani z dorzucaniem warstw abstrakcji jako takich. Natomiast kiedy nie widzę żadnych korzyści z dorzucenia dodatkowej warstwy abstrakcji to tego nie robię - inaczej bym musiał wrzucać wszędzie interfejsy i dopiero do nich pisać implementacje.

5
RequiredNickname napisał(a):

Kiedyś w aplikacjach, dziś dumnie określanych jako legacy, też nie stosowano się do reguł/dobrych praktyk (jedne ignorowano celowo, inne np nie były jeszcze wtedy sformalizowane) za to stosowano takie względem ktorych nie wiedziano że się zemszczą i drastycznie utrudnia późniejsze utrzymanie (klasyczny crud warstwowy z logiką wymieszaną po encjach i serwisach w całej aplikacji bez żadnej enkapsulacji i testów).

Dokłanie: Pracowałem z aplikacjami robionymi jeszcze w latach 90-tych.. był taki czas (2000 - 2002), że singleton nie był powszechnie uznawany za antypattern :-) - wręcz przeciwnie. A dzisiejszy bardzo dobry kod byłby uznany dość często za tragiczny (za mało komentarzy, brak setterów, getterów - o rany).

A co do tematu:
uważam, że przeważnie nie ma sensu wprowadzania tego typu warstw abstrakcji przy Springu.
To inwazywny framework, który zmienia (wypacza :-( ) działanie całego kodu, nawet jeśli adnotacji nie widać.

Widziałem kilkukrotnie jak wprowadzanie warstw (właśnie w perzystencji) tylko pogłebia chaos walki z typowymi problemami springa (a nawet bardziej JPA), ginie kontekst transakcji, security, encje się "detachują" itd. A to wszystko jest jeszcze osrane frameworkiem robiącym debilne 1-1 mapowania z DTOsów na DBosy na PIERDUosy.

Trafiłem kiedyś do jednego projektu, który totalnie na takich problemach utkwił i dokładnie szybki ratunek polegał na wycięciu warstw - po wycięciu aplikacja zaczęła na tyle działać, że zespól mógł wrócić do pracy i dodawać ficzery.

Z drugiej strony - przeżyłem kilka dużych (robionych miesiącami/latami) ewolucji aplikacji, gdzie zmieniano warstwy perzystencji, prezentacji itp.
NIGDY, żadne zrobione przez architeków abstrakcje nie były przygotowane na to co chcieliśmy zmienić, a tylko utrudniały :-), zwykle zmieniała się cała filozofia działania danej warstwy. Weźmy choćby różnicę jaka występuje jak masz JPA/dirty check vs JOOQ i explicite zapis do bazy vs Monada IO - tych warstw pośrednich - repo czy czegoś tam nawet nie da się w "zbliżony" sposób napisać.

0

Jednym z korzyści posiadania kolejnej warstwy jest to, że możesz zwrócić np. Either<Exception, Domain.User>. Baza danych jest zewnętrznym serwisem i wiadomo, że wywolwanie może się wysypać. Rozumiem argumenty, że framework się nie zmieni, baza danych się nie zmieni itd., ale jednak sposób w jaki projekt jest napisany może się zmienić tj. pójdzie w kierunku programowania funkcyjnego, ponieważ już nikt nie będzie wiedział który 'exception handler' łapie który wyjątek (Ale to jest wina Springa, i jego 'modelu programowania').

Ja z warstwy 'repository' nigdy nie zwracam nic co ma cokolwiek wspólnego z bazą danych.

Podsumowując, ja położył bym warstwę abstrakcji, złapał potencjalny wyjątek i zwrócił Either<Exception, Domain.User>

0
dmw napisał(a):

Jednym z korzyści posiadania kolejnej warstwy jest to, że możesz zwrócić np. Either<Exception, Domain.User>. Baza danych jest zewnętrznym serwisem i wiadomo, że wywolwanie może się wysypać. Rozumiem argumenty, że framework się nie zmieni, baza danych się nie zmieni itd., ale jednak sposób w jaki projekt jest napisany może się zmienić tj. pójdzie w kierunku programowania funkcyjnego, ponieważ już nikt nie będzie wiedział który 'exception handler' łapie który wyjątek (Ale to jest wina Springa, i jego 'modelu programowania').

Ja z warstwy 'repository' nigdy nie zwracam nic co ma cokolwiek wspólnego z bazą danych.

Podsumowując, ja położył bym warstwę abstrakcji, złapał potencjalny wyjątek i zwrócił Either<Exception, Domain.User>

Eithera raczej powinno się zwracać z serwisów

1 użytkowników online, w tym zalogowanych: 0, gości: 1