Wątek przeniesiony 2023-11-28 18:09 z Java przez Riddle.

Wady i zalety osobnego interfejsu na service

0

Witam

mam pytanie to reszty bardziej doświadczonych kolegów z forum. Jaką macie opinie o Service I ServiceImpl - mowa tylko o wyłącznie Servisie, który implementuje jeden interfejs.
Wspomniany interfejs nie jest implementowany przez inną klasę.

Są jakiekolwiek zalety takiego rozwiązania?
Szukałem na internecie i niestety nie udało mi się znaleźć odpowiedzi. Opinie były podzielone, wręcz co dziwne to liczba głosów rozkłada się prawie na
50% tak, 50% nie.

Ja osobiście nie do końca rozumiem sens, aby tak robić, a wy jakie rozwiązania stosujecie u siebie?
Byłby ktoś w stanie jakoś uzasadnić takie rozwiązanie?

// Edit
Chciałbym tylko dodać, że mówimy tylko o klasach, które nie będą w żaden sposób rozszerzane
Interfejs będzie implementowany tylko zawsze przez jedną klasę

Dzięki i pozdrawiam

3

Robienie interfejsów do serwisów to jedyny natywny sposób umożliwiający ich mockowanie w testach, bez tego trzeba polegać na 3rd party frameworkach typu mockito.
Poza tym jest zgodne z zasadami solid, zwiększa elastyczność kodu, wprowadza dependency inversion i luzuje zależności między kodem, interfejsy można wydzielić w osobną bibliotekę żeby uniknąć cyklicznych zależności.
Jeśli będziesz chciał na przykład kiedyś owrappować ServiceImpl w powiedzmy warstwę cache'ującą możesz to zrobić w jednym miejscu zamieniając tylko reguły wstrzykiwania zależności zamiast poprawiać kod w wielu miejscach lub komplikując kod serwisu wprowadzając cache w nim samym.
To dość rzadkie sytuacje ale się zdarzają, a dodanie interfejsu raczej nikomu nie przeszkadza - dodaje też czysty widok na interfejs, wiemy jakie ma metody, nie musimy scrollować całego pliku żeby zobaczyć co serwis oferuje.
Możemy też napisać sam interfejs i zlecić koledze z zespołu jego zaimplementowanie.

Nie podoba mi się za to nazewnictwo Service / ServiceImpl, w ramach możliwości lepiej interfejs nazwać po tym co ma robić np UserService a klasę implementującą po konkretnych szczegółach implementacji np DatabaseUserService / PersistentUserService.

7
obscurity napisał(a):

Robienie interfejsów do serwisów to jedyny natywny sposób umożliwiający ich testowanie, bez tego trzeba polegać na 3rd party frameworkach typu mockito.

To ciekawostka. Sugerujesz, że aby przetestować ServiceImpl muszę mieć interfejs Service - otóż nie.
Co więcej po prostu nie - nie trzeba mieć ani mockito, ani mocków, ani żadnych interfejsów i implów żeby normalnie testować.

EDIT (see komentarz)
Przeważnie nie ma potrzeby żadnego mockowania i można po prostu testować kod taki jak idzie na produkcję.
W tych miejscach gdzie faktycznie potrzebujemy mocka (rzadko) można sobie zrobić interfejs - wtedy kiedy zajdzie taka potrzeba.
YAGNI

3
pitagram napisał(a):

Wspomniany interfejs nie jest implementowany przez inną klasę.

To nie rozdzielaj. To ma sens np. przy pisaniu bibliotek, gdzie użytkownik twojej biblioteki nie uzależni się (a przynajmniej nie tak łatwo) od konkretnej implementacji twojej klasy.
A jeśli twoja klasa jest używana tylko w 1 projekcie, który ty kontrolujesz, to tym bardziej nie rozdzielaj - bo zawsze możesz zrobić to później, kojarzę nawet że IntelliJ chyba ma gotowy mechanizm do tego.

6
obscurity napisał(a):

Robienie interfejsów do serwisów to jedyny natywny sposób umożliwiający ich mockowanie w testach, bez tego trzeba polegać na 3rd party frameworkach typu mockito.

nie wiem o co chodzi dokładnie z tym "jedynym sposobem", ale można zawsze zrobić testową podklasę i nadpisywać metody i tak robię jeśli trzeba (ogólnie rzadko). w javie wszystkie metody, które są jednocześnie niestatyczne i nieprywatne, są wirtualne, więc można je bezproblemowo nadpisywać (w przeciwieństwie do c# czy c++, które do umożliwienia nadpisywania wymagają słówka virtual, którego programiści się chyba boją), więc nie trzeba cargo cultów typu interfejs + jedyna jego implementacja tuż obok. po co sobie duplikować kod niepotrzebnie? java jest wystarczająco rozwlekła :) kiedyś te niepotrzebne interfejsy mnie na tyle zirytowały, że powywalałem je ze zdecydowanej większości aktywnie rozwijanych mikroserwisów w firmie i teraz praktycznie się ten cargo cult nie odradza.

problemem może być wymagany konstruktor, który robi jakiś efekt uboczny (inny niż zwykłe wypełnianie pól) i wtedy dziedzicząc trudno uniknąć jego odpalenia (w testowej podklasie), ale dla mnie takie konstruktory to antywzorzec. kiedyś był bardzo dobry artykuł o tym http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/ ale coś mi nie działa. w skrócie: wszelakie efekty uboczne trzeba przenieść do metod fabrykujących (czyli do statycznych metod lub, jeśli ktoś chce iść w wyższe abstrakcje, do abstrakcyjnych fabryk).

8

Zaszłość historyczna, nie ma to sensu i nie powinno być nigdy wykorzystywane.
Interfejsy z jedną implementacją mają natomiast sens gdy projektujemy architekturę np. port-and-adapters i wtedy port zazwyczaj (choć nie zawsze) ma jedną implementację.

0
Wibowit napisał(a):

w javie wszystkie metody, które są jednocześnie niestatyczne i nieprywatne, są wirtualne, więc można je bezproblemowo nadpisywać (w przeciwieństwie do c# czy c++, które do umożliwienia nadpisywania wymagają słówka virtual, którego programiści się chyba boją)

Masz rację zależy od języka, faktycznie w javie wydaje się to nie mieć sensu, natomiast w kotlinie znowu metody nie są domyślnie wirtualne i moim zdaniem wprowadza to większy porządek i enkapsulacje, open by design, closed by default; zależy jak jesteś zafiksowany na tym punkcie. Ja na przykład lubię jak mam klasę, widzę kod i jestem pewny że to ten kod się wykona, bez żadnych "ale" że ktoś jednak ten kod całkowicie zmienił w nieznanym mi miejscu. Jak coś ma być otwarte na zmiany to jest to zaznaczone w jasny sposób.
Nie tyle się boją co metody virtual są wolniejsze bo wymagają VMT, a zaleca się nawet zamykanie całych klas na dziedziczenie bo kompilator może wprowadzić dodatkowe optymalizacje, a nawet dynamiczny kod używający refleksji też może pominąć niektóre checki, nie mówiąc o tym że taki kod jest po prostu prostszy do ogarnięcia przez programistę. Celem javy natomiast nigdy chyba nie była szybkość działania.

2
obscurity napisał(a):

Nie tyle się boją co metody virtual są wolniejsze bo wymagają VMT, a zaleca się nawet zamykanie całych klas na dziedziczenie bo kompilator może wprowadzić dodatkowe optymalizacje, a nawet dynamiczny kod używający refleksji też może pominąć niektóre checki, nie mówiąc o tym że taki kod jest po prostu prostszy do ogarnięcia przez programistę.

To nie do końca prawda. Metody wirtualne nie wymagają VMT ( Z tego co wiem, to również w C++. VMT nie jest częścią specyfikacji języka C++, to po prostu szczegół implementacyjny kompilatora).
Co więcej w javie metody wirtualne mogą być i są inlinowane w większości JVM (oczywiście tylko jeśli to ma sens, bo mamy w danym momencie jedną implementację - ewentualnie kilka implementacji).
Z dowcipów: metoda uprzednio inlinowana może przestać być inlinowana jeśli w trakcie działania JVM zostaną podładowane dynamicznie klasy z alternatywnymi implementacjami.
(Oczywiście, jeżeli metoda jest faktycznie "dość wirtualna" czyli ma ileś implementacji to gdzieś ten narzut na wybór implementacji się pojawi).

Ze smutnych rzeczy -> java/jvm to na tyle dynamiczny jezyk/platforma (klasy mogą być "podmieniane" i doładowywane w trakcie działania), że wszelkie pomaganie kompilatorowi praktycznie mija się z celem -> bo i tak najważniejscze optymalizacje mogą być dokonywane dopiero w trakcie działania (i to również dynamicznie, czyli uprzednio skompilowany kod maszynowy bywa wyrzucany i kompilowany od nowa (i to czasem wielokrotnie)).

Celem javy natomiast nigdy chyba nie była szybkość działania.

Bo szybkość działania to jeden z celów twórców JVM. Java to język programowania.

Zasadniczo czy wydzielać interfejs czy nie nie ma specjalnego wpływu na szybkość działania kodu (w nowszych JVM (czyli tak gdzieś od 20 lat)). To jest decyzja projektowa. Tak samo jak tworzenie klas final.

Btw. popieram zdecydowanie zamykanie wszystkiego. Nie tylko by default, ale zawsze (z nielicznymi wyjątkami, gdy jednak używanie podtypów pozwala znacznie uprościć kod (np. w kotlinie używam "dziedziczenia" do DI).

1
obscurity napisał(a):
Wibowit napisał(a):

w javie wszystkie metody, które są jednocześnie niestatyczne i nieprywatne, są wirtualne, więc można je bezproblemowo nadpisywać (w przeciwieństwie do c# czy c++, które do umożliwienia nadpisywania wymagają słówka virtual, którego programiści się chyba boją)

Masz rację zależy od języka, faktycznie w javie wydaje się to nie mieć sensu, natomiast w kotlinie znowu metody nie są domyślnie wirtualne i moim zdaniem wprowadza to większy porządek i enkapsulacje, open by design, closed by default; zależy jak jesteś zafiksowany na tym punkcie.

ah, final by default w kotlinie. no można i tak. modyfikator open na klasie przywraca javowy tryb open by default.

scala 3 też ma tego typu modyfikatory: https://docs.scala-lang.org/scala3/reference/other-new-features/open-classes.html z tym, że są 3 stany:

  • final i nie można dziedziczyć
  • open, który oznacza, że klasa jest w pewnym sensie przygotowana do dziedziczenia jej
  • brak modyfikatora oznacza, że przy dziedziczeniu jej kompilator rzuci ostrzeżenie, które można wyłączyć za pomocą import scala.language.adhocExtensions lub podobnego mechanizmu

java 17 ma sealed classes i wtedy można robić hierarchie z nowymi słówkami kluczowymi (uwaga: sealed w javie to coś innego niż sealed w c#. odpowiednikiem sealed z c# może być np. javowy final).
https://openjdk.org/jeps/409

package com.example.geometry;

public abstract sealed class Shape
    permits Circle, Rectangle, Square, WeirdShape { ... }

public final class Circle extends Shape { ... }

public sealed class Rectangle extends Shape 
    permits TransparentRectangle, FilledRectangle { ... }
public final class TransparentRectangle extends Rectangle { ... }
public final class FilledRectangle extends Rectangle { ... }

public final class Square extends Shape { ... }

public non-sealed class WeirdShape extends Shape { ... }

Ja na przykład lubię jak mam klasę, widzę kod i jestem pewny że to ten kod się wykona, bez żadnych "ale" że ktoś jednak ten kod całkowicie zmienił w nieznanym mi miejscu. Jak coś ma być otwarte na zmiany to jest to zaznaczone w jasny sposób.

dla mnie metody domyślnie niewirtualne (nie mylić z finalami itp - tutaj mówię o różnych implementacjach polimorfizmu, a nie projektowaniu docelowego kodu) i ich obrona to stawianie sprawy na głowie i to jest wada zarówno c# jak i c++. pilnowanie, by implementacje metod w danej klasie współgrały dobrze ze sobą to zadanie autora tej klasy, a nie wszystkich ludzi, którzy korzystają z tej klasy. c# ma takiego potworka jak https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/new-modifier który przesłania metodę z nadklasy o takiej samej sygnaturze i wtedy w zależności od typu referencji (podklasa vs nadklasa) w miejscu wywołania metody, inna implementacja może zostać wywołana (c++ ma podobnie jeśli nie użyje się virtual). jako użytkownika klasy nie powinno mnie w ogóle interesować jakiego typu referencji używać, by operować klasą poprawnie. typ referencji powinien mi mówić tylko jakie metody mogę wywołać, a nie które konkretnie implementacje będą wywołane.

Nie tyle się boją co metody virtual są wolniejsze bo wymagają VMT, a zaleca się nawet zamykanie całych klas na dziedziczenie bo kompilator może wprowadzić dodatkowe optymalizacje, a nawet dynamiczny kod używający refleksji też może pominąć niektóre checki, nie mówiąc o tym że taki kod jest po prostu prostszy do ogarnięcia przez programistę. Celem javy natomiast nigdy chyba nie była szybkość działania.

metody z interfejsów to też metody wirtualne i dlatego w c# (który słabo radzi sobie z dewirtualizacją) używa się typu List<T> (konkretna klasa), a nie interfejsu IList<T>. poza tym, c# 8.0 ma https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/default-interface-methods a więc implementacje metod w interfejsach, które dodatkowo mogą być nadpisywane w podinterfejsach. gdzieś czytałem o tym, że wirtualne wywołania przez interfejsy mogą być szybsze niż przez klasy, tzn. że vtable dla interfejsów jest w jakiś sposób szybsze w obsłudze niż vtable dla klasy, ale kiedyś szukałem i nie mogłem znaleźć szczegółów. w każdym razie, do pełni szybkości działania trzeba przede wszystkim inline'owania, które nie zadziała jeśli metoda nie jest zdewirtualizowana. podusmowując: do osiągnięcia pełni wydajności gdy mamy wirtualne metody (czy to z interfejsów czy z klas) trzeba je najpierw zdewirtualizować, a potem zinline'ować. benchmarki pokazujące wyższą szybkość wywołań wirtualnych z interfejsów niż z klas to tutaj marny dowcip, bo kluczem do odblokowania pełni wydajności jest zawsze inline'owanie (w ogólności, bo czasem inline'owanie się nie opłaca).

java od wieków ma głęboką dewirtualizację, eliminację alokacji na stercie (jeśli wykryje, że obiekt nie ucieka poza lokalny zakres) itd, a po tym wszystkim zachodzi inline'owanie i mamy pełnię wydajności wysokopoziomowego kodu. dewirtualizacja jest zarówno taka bardziej dynamiczna (bo wynikająca z profilowania), jak dynamiczna optymalizacja wywołań monomorficznych, ale i taka nieco bardziej statyczna (bo wynikająca z analizy metadanych), jak class hierarchy analysis. dzięki class hierarchy analysis jvm od razu widzi, które metody są nadpisane, a które nie i od razu traktuje te nigdzie nienadpisane jako niewirtualne. oczywiście jeśli załaduje gdzieś podklasę, która faktycznie nadpisuje te nienadpisywane wcześniej metody to jvm przeprowadza deoptymalizację odpowiednich kawałków kodu, by je potem optymalizować ponownie, dopasowując się do nowej sytuacji. dynamiczna deoptymalizacja w javie umożliwia agresywne optymalizacje, tzn. takie które mogą być zbyt mocne (jak to założenie o nienadpisywaniu metod) i trzeba je odkręcać.

prostym testem dla class hierarchy analysis jest porównanie wydajności dostępu do pól: dostęp do pól bezpośrednio (czyli mamy np. pola typu public final) kontra dostęp przez gettery i settery wirtualne (tzn. publiczne, bez finala), ale bez polimorfizmu (tzn. hierarchii klas czy interfejsów). wydajność jest identyczna, chociaż naiwna analiza wskazywałaby, że "skoro metody są wirtualne to muszą być koszmarnie wolne".

na koniec podam przykład na to, że dewirtualizacja, escape analysis i inne optymalizacje są w javie już od bardzo dawna: Dlaczego foreach jest gorsze od for (beta)
co prawda przykład jest na tablicy zaboxowanych longów, ale liczy się co jest odpalane dla każdego pojedynczego elementu.

3
obscurity napisał(a):

Robienie interfejsów do serwisów to jedyny natywny sposób umożliwiający ich mockowanie w testach, bez tego trzeba polegać na 3rd party frameworkach typu mockito.

No na pewno nie jedyny.

obscurity napisał(a):

Poza tym jest zgodne z zasadami solid, zwiększa elastyczność kodu, wprowadza dependency inversion i luzuje zależności między kodem, interfejsy można wydzielić w osobną bibliotekę żeby uniknąć cyklicznych zależności.

Samo dodanie interfejsu nie oznacza jeszcze Dependency Inversion. Interfejs też może być "ubrudzony" szczegółami implementacyjnymi, i wtedy lipa z takiego Dependency Inversion.

pitagram napisał(a):

Witam

mam pytanie to reszty bardziej doświadczonych kolegów z forum. Jaką macie opinie o Service I ServiceImpl - mowa tylko o wyłącznie Servisie, który implementuje jeden interfejs.
Wspomniany interfejs nie jest implementowany przez inną klasę.

Są jakiekolwiek zalety takiego rozwiązania?
Szukałem na internecie i niestety nie udało mi się znaleźć odpowiedzi. Opinie były podzielone, wręcz co dziwne to liczba głosów rozkłada się prawie na
50% tak, 50% nie.

Ja osobiście nie do końca rozumiem sens, aby tak robić, a wy jakie rozwiązania stosujecie u siebie?
Byłby ktoś w stanie jakoś uzasadnić takie rozwiązanie?

// Edit
Chciałbym tylko dodać, że mówimy tylko o klasach, które nie będą w żaden sposób rozszerzane
Interfejs będzie implementowany tylko zawsze przez jedną klasę

Sama zasadność dodania interfejsów do "warstwy" serwisów, celem oddzielenia ich od np biblioteki HTTP albo od bazy nawet ma sens. Tylko to ma sens wtedy, kiedy faktycznie chcesz oddzielić te warstwy od siebie - w taki sposób, że mógłbyś jedną z nich usunąć, i druga nadal by działała (z minimalnym postawieniem). Wtedy tak.

Tylko że w "zwykłych" aplikacjach webowych nikt praktycznie tak nie robi - dodaje się wtedy interfejs pomiędzy dwie warstwy, ale tak na prawdę ten interfejs nadal jest "ubrudzony" szczegółami implementacyjnymi tego serwis, więc o żadnym oddzieleniu nie ma mowy. Ludzie dodają taki pół środek (nawet nie, ćwierć środek), w stylu "dodamy interfejs, żeby mieć DI" - "ale to nadal nie pozwoli dobrze podmienić implementacji" - "aaaj tam". Więc często w takich zespołach nie można usunąć tego nadmiarowe interfejsu, bo ludzie mówią "zostaw, chcemy mieć DI"; ale nie można tego go "wyczyścić" ze szczegółów implementacyjnych, bo wtedy ludzie znowu mówią "aaa, nie potrzeba".

Prawda jest taka, że żeby dobrze oddzielić dwie warstwy od siebie, to nie jest łatwe - trzeba umieć to robić i widzieć co i po co się to robi. To nie jest takie proste jak "użyj interfejs tutaj zamiast klasy". Tylko jak mówiłem - to nie jest łatwe, i nie jest takie 0/1kowe żeby to dodać. Geneza tego podejścia najczęściej jest taka, że ktoś gdzieś usłyszał że "interfejsy są dobre", więc je dodał - ale nie do końca kuma zasadność ich działania tutaj.

Także jeśli mowa jest o takiej "zwykłej" apce, to Service/ServiceImpl najpewniej nie ma sensu.

PS: To jest bardzo częsty problem w laravelu w PHP - jest taka konwencja ze repozytoria mają mieć impl oraz interfejs - tylko że te interfejsy, i tak w zwracanym typie mają DTO z bazy. Więc o żadnym Dependency Inversion nie może być mowy w takim czymś.

tl;dr; Interfejsy z jedną implementacją mają sens, jak chcesz oddzielić dwie zależności od siebie. Ale kiedy nie są oddzielone (np ciągle jest szczegól implementacyjny w interfejsie), to wtedy IMO nie mają sensu.

A odpowiadając na pozostały post od @obscurity

obscurity napisał(a):

Poza tym jest zgodne z zasadami solid, zwiększa elastyczność kodu, wprowadza dependency inversion i luzuje zależności między kodem, interfejsy można wydzielić w osobną bibliotekę żeby uniknąć cyklicznych zależności.

jest zgodne z zasadami solid, zwiększa elastyczność kodu, wprowadza dependency inversion i luzuje zależności między kodem - Tak, ale tylko jeśli się to wprowadzi odpowiednio (czyli interfejs nie ma szczegółów implementacynych), a w 95% przypadków nie jest wprowadzone odpowiednio.

To jest jedyna dobra rada której udzieliłeś w tym poście.

obscurity napisał(a):

Jeśli będziesz chciał na przykład kiedyś owrappować ServiceImpl w powiedzmy warstwę cache'ującą możesz to zrobić w jednym miejscu zamieniając tylko reguły wstrzykiwania zależności zamiast poprawiać kod w wielu miejscach lub komplikując kod serwisu wprowadzając cache w nim samym.

A o YAGNI Pan słyszał? Nie robi się interfejsów teraz, po to żeby BYĆ MOŻE w przyszłości dodać cache. Nie przewidzisz wszystkich przypadków, tego co trzeba będzie zrobić w przyszłości, więc bez sensu coś dodawać tylko pod ten potencjalny przypadek.

obscurity napisał(a):

To dość rzadkie sytuacje ale się zdarzają, a dodanie interfejsu raczej nikomu nie przeszkadza - dodaje też czysty widok na interfejs, wiemy jakie ma metody, nie musimy scrollować całego pliku żeby zobaczyć co serwis oferuje.

Rozwiązaniem problemu "scrollowania dużego pliku" nie jest wydzielenie interfejsu, tylko podział klasy na mniejsze elementy.

obscurity napisał(a):

Możemy też napisać sam interfejs i zlecić koledze z zespołu jego zaimplementowanie.

Zły pomysł. Wydłużasz feedback loop.

3

Ma sens taki podział jeśli interface Service będzie w module client / api i będzie zaciągnięty po stronie klienta, a implementacja będzie w osobnym module, niedostępnym dla klienta (serwisu, który używa nas serwis).

0

Przydaję się to podejście przy TDD, bo wtedy sobie napiszesz metody w takim interfejsie bez potrzeby implementacji w serwisie, wykorzystasz interfejs w teście i masz czyste test case'y

10
Escanor16 napisał(a):

Przydaję się to podejście przy TDD, bo wtedy sobie napiszesz metody w takim interfejsie bez potrzeby implementacji w serwisie, wykorzystasz interfejs w teście i masz czyste test case'y

O! To bardzo ciekawy pomysł i zupełnie bez sensu.

1
Escanor16 napisał(a):

Przydaję się to podejście przy TDD, bo wtedy sobie napiszesz metody w takim interfejsie bez potrzeby implementacji w serwisie, wykorzystasz interfejs w teście i masz czyste test case'y

To co te testy by niby testowały? -.- Bo testy bez implementacji trochę nie mają sensu.

0
Riddle napisał(a):
obscurity napisał(a):

Możemy też napisać sam interfejs i zlecić koledze z zespołu jego zaimplementowanie.

Zły pomysł. Wydłużasz feedback loop.

W jaki sposób? Parukrotnie zastosowaliśmy to w zespole i mogliśmy zrównoleglić robienie taska na dwie osoby dzięki czemu zajął on parę godzin zamiast całego dnia. Co prawda można było się obyć bez żadnego interfejsu, po prostu był w tym przypadku szybkim sposobem na opisanie wymagań.

Riddle napisał(a):

To jest jedyna dobra rada której udzieliłeś w tym poście.

tak naprawdę to nie były nawet rady, opisałem tylko co potencjalnie mogło kierować osobą która zrobiła taki podział. Osobiście stosuję tylko gdy jest taka potrzeba, na pewno nie robię interfejsu do każdego serwisu, w przypadku pisania biblioteki staram się trzymać wszystko zamknięte ale jak najwięcej rzeczy rozszerzalnych przez proste interfejsy. W javie może mieć to mniejszy sens bo jak już wspomniano i tak wszystko można sobie nadpisać i zamiast zaimplementować interfejs user może po prostu dziedziczyć po klasie i olać jej całą implementację.

0
obscurity napisał(a):
Riddle napisał(a):
obscurity napisał(a):

Możemy też napisać sam interfejs i zlecić koledze z zespołu jego zaimplementowanie.

Zły pomysł. Wydłużasz feedback loop.

W jaki sposób? Parukrotnie zastosowaliśmy to w zespole i mogliśmy zrównoleglić robienie taska na dwie osoby dzięki czemu zajął on parę godzin zamiast całego dnia. Co prawda można było się obyć bez żadnego interfejsu, po prostu był w tym przypadku szybkim sposobem na opisanie wymagań.

Jeśli cokolwiek mogłeś zyskać, w ten sposób to max. 2x usprawnienia (czyli jesli coś miało zająć 4h, to w dwie osoby zajmie 2h), to jest raz. A dwa, że owszem, gdyby takie podzielenie nie niosło za sobą żadnych narzutów to możesz sobie zrównoleglić takie dewelopowanie.

Tylko dokładnie przez to co napisałem - wydłużony feedback loop - narzuty są. Ten pomysł zakłada że tworząc interfejs przewidzisz dokładnie jak on ma wyglądać, metody, nazwy, parametry, typy, usecase'y, etc - takie które inny teammate może wytworzyć.

I przy dużych projektach, po pierwsze ciężko jest coś takiego wymyśleć, a po drugie za wiele to też nie wnosi. Dużo lepiej to zrobić albo samemu, albo zrobić pair programming. Napisać interfejs i dać komuś, kto się musi w niego wstrzelić to słaby pomysł. Chyba że on może też go edytować, ale to by znaczyło że musielibyście integrować swoje zmiany, co znowu wydłużą feedback loop.

Nie worth. Są lepsze sposoby żeby to zrobić.

Takie coś może mieć chyba tylko i wyłącznie sens, jeśli ten komu to dajesz to jest jakiś słaby junior który nie umie zrobić nic oprócz zaimplementowania interfejsu które się mu da (tylko wtedy co to za improvement jest).

0
Riddle napisał(a):

Tylko dokładnie przez to co napisałem - wydłużony feedback loop - narzuty są. Ten pomysł zakłada że tworząc interfejs przewidzisz dokładnie jak on ma wyglądać, metody, nazwy, parametry, typy, usecase'y, etc - takie które inny teammate może wytworzyć.

To był element pair programmingu, po prostu wspólnie pisaliśmy klasę tak jakby serwis już istniał, gdy był praktycznie gotowy wydzieliliśmy interfejs i podzieliliśmy pracę gdzie ja mogłem się skupić na refaktoringu, testach i optymalizacji a kolega na strukturze bazy i zapytań.
Nie było moim zdaniem żadnego narzutu, chyba że liczyć pair programming jako narzut.
Natomiast przyznaję że robiliśmy to bardziej w formie zabawy niż żeby faktycznie uzyskać cel szybciej, różnica paru godzin zyskanych w sprincie nikogo nie zbawi.

5

Metody publiczne klasy to też dobry interfejs.
A zamiast się bawić można wpisać w jakieś dodatkowe twory można wpisać domyślną implementację:

class C {
  public X method1() {
      throws new UnsupportedOperationException();
  }
}

nadaje się i do rozdzielania pracy i do TDD.

0

Nawiązując do dyskusji, mam pytanie. Tworzę projekt w ramach nauki programowania. Do tej pory robiłem właśnie tak, żeby zachować DI tworzyłem interface z metodami i klasę, która go implementuje. Ta klasa jest package-private i znajduję się wraz z interfacem w tym samym pakiecie auth -> service. Czy takie coś może być? Bo przeczytałem ten wątek i z tego co zrozumiałem jest to błąd, że powinny być dwa moduły. Czyli mam przebudować projekt na coś takiego ?

moduł-api -> src -> main
moduł-serwis -> src -> main

Przykładowy interface i klasa:

public interface RegistrationService {
    ApiResponse register(RegisterRequest registerRequest) throws DatabaseException;
}


@Slf4j
@Service
class RegistrationServiceImpl implements RegistrationService {
    private final UserDAO userDAO;
    private final ValidationService validationService;
    private final ApplicationMessageService messageService;
    private final UserRoleManager userRoleManager;
    private final UserFactory userFactory;
    private final CredentialExistenceChecker credentialExistenceChecker;

    public RegistrationServiceImpl(UserDAO userDAO, ValidationService validationService,
                                   ApplicationMessageService messageService,
                                   UserRoleManager userRoleManager, UserFactory userFactory,
                                   CredentialExistenceChecker credentialExistenceChecker) {
        this.userDAO = userDAO;
        this.validationService = validationService;
        this.messageService = messageService;
        this.userRoleManager = userRoleManager;
        this.userFactory = userFactory;
        this.credentialExistenceChecker = credentialExistenceChecker;
    }

    @Transactional
    @Override
    public ApiResponse register(RegisterRequest registerRequest) throws DatabaseException {

        validationService.validateCredentials(registerRequest);

        credentialExistenceChecker.checkForExistingCredentials(registerRequest);

        Role role = userRoleManager.getRoleByName(UserRole.ROLE_USER);

        User user = userFactory.createUser(registerRequest, role);

        try {
            userDAO.save(user);
        } catch (DataAccessException ex) {
            throw new DatabaseException(messageService.getMessage("user.registration.error"));
        }
        return new ApiResponse(messageService.getMessage("user.registration.success"));
    }
}


0
Ornstein napisał(a):

Przykładowy interface i klasa:

public interface RegistrationService {
    ApiResponse register(RegisterRequest registerRequest) throws DatabaseException;
}

Pokażesz te RegisterRequest, DatabaseException oraz ApiResponse?

0

@Riddle:

public record RegisterRequest(@NotEmpty(message = "The username field cannot be empty.")
                              String username,
                              @NotEmpty(message = "The email field cannot be empty")
                              String email,
                              @NotEmpty(message = "The password field cannot be empty")
                              String password) {
}


public class DatabaseException extends ApplicationException {
    public DatabaseException(String message) {
        super(message);
    }
}

public record ApiResponse(String message) {
}

DatabaseException rozszerza ApplicationException:

public class ApplicationException extends Exception {
    public ApplicationException(String message) {
        super(message);
    }
}

0

@Ornstein: DatabaseException jest spoko. A to @NotEmpty to skąd? To jest javax.validation.*? Czy z frameworka HTTP?

0

@Riddle: To jest z javax.validation.*.

0
Ornstein napisał(a):

@Riddle: To jest z javax.validation.*.

Okej, no to to jest taka gray area. Twój interfejs nadal ma zalezność, właśnie na javax.validation.*. Ale nie ma przynajmniej na libkę od HTTP i nie na bazę danych, więc nie jest źle. Jeśli chciałbyś wprowadzić takie na prawdę porządne dependency inversion, to zależność na to @NotEmpty też powinieneś złamać.

4
pitagram napisał(a):

mam pytanie to reszty bardziej doświadczonych kolegów z forum. Jaką macie opinie o Service I ServiceImpl - mowa tylko o wyłącznie Servisie, który implementuje jeden interfejs.

Jest to code smell.

obscurity napisał(a):

Jeśli będziesz chciał na przykład kiedyś owrappować ServiceImpl w powiedzmy warstwę cache'ującą możesz to zrobić w jednym miejscu zamieniając tylko reguły wstrzykiwania zależności zamiast poprawiać kod w wielu miejscach lub komplikując kod serwisu wprowadzając cache w nim samym.

Nie potrzeba do tego interfejsów.

To dość rzadkie sytuacje ale się zdarzają, a dodanie interfejsu raczej nikomu nie przeszkadza - dodaje też czysty widok na interfejs, wiemy jakie ma metody, nie musimy scrollować całego pliku żeby zobaczyć co serwis oferuje.

A to inny problem - serwisy na ogół są bez sensu, lepiej mieć małe klasy robiące jedną rzecz. Wtedy publicznym interfejsem jest jedna metoda.

2
somekind napisał(a):

serwisy na ogół są bez sensu, lepiej mieć małe klasy robiące jedną rzecz. Wtedy publicznym interfejsem jest jedna metoda.

Amen. I nie trzeba do tego cqrsa.

1
Riddle napisał(a):
Ornstein napisał(a):

@Riddle: To jest z javax.validation.*.

Okej, no to to jest taka gray area. Twój interfejs nadal ma zalezność, właśnie na javax.validation.*. Ale nie ma przynajmniej na libkę od HTTP i nie na bazę danych, więc nie jest źle. Jeśli chciałbyś wprowadzić takie na prawdę porządne dependency inversion, to zależność na to @NotEmpty też powinieneś złamać.

Na użycie Stringa, Inta czy HashMapy też wprowadzasz dodatkowy interfejs - żeby nie było zależności? Przecież to warstwa niższa nawet niż HTTP czy DAO.

Oczywiscie javax.validation.* to g**no, jak całe JavaEE i pochodne, ale wprowadzanie sztucznych zależności uważam za bzdurę taką samą jak pattern interfejs - impl.

0
jarekr000000 napisał(a):
Riddle napisał(a):
Ornstein napisał(a):

@Riddle: To jest z javax.validation.*.

Okej, no to to jest taka gray area. Twój interfejs nadal ma zalezność, właśnie na javax.validation.*. Ale nie ma przynajmniej na libkę od HTTP i nie na bazę danych, więc nie jest źle. Jeśli chciałbyś wprowadzić takie na prawdę porządne dependency inversion, to zależność na to @NotEmpty też powinieneś złamać.

Na użycie Stringa, Inta czy HashMapy też wprowadzasz dodatkowy interfejs - żeby nie było zależności?

Nie. To by było głupie.

Przecież to warstwa niższa nawet niż HTTP czy DAO.

Nie chodzi o warstwy wyżej/niżej. Chodzi o to żeby w swojej domenie mieć rzeczy które nie kopną nas, kiedy coś się popsuje. I z reguły rzeczy wbudowane w język takie jak int/string takie są , więc nie ma sensu się od nich oddzielać.

1
Riddle napisał(a):

Nie chodzi o warstwy wyżej/niżej. Chodzi o to żeby w swojej domenie mieć rzeczy które nie kopną nas, kiedy coś się popsuje. I z reguły rzeczy wbudowane w język takie jak int/string takie są , więc nie ma sensu się od nich oddzielać.

A gdzie jest granica? Bo jak piszesz w JavaEE (czyli również w Spring) to takie rzeczy jak adnotacje javax.validation czy też @Component , @Service itp. to po prostu twój jezyk / platoforma.

0
jarekr000000 napisał(a):
Riddle napisał(a):

Nie chodzi o warstwy wyżej/niżej. Chodzi o to żeby w swojej domenie mieć rzeczy które nie kopną nas, kiedy coś się popsuje. I z reguły rzeczy wbudowane w język takie jak int/string takie są , więc nie ma sensu się od nich oddzielać.

A gdzie jest granica? Bo jak piszesz w JavaEE (czyli również w Spring) to takie rzeczy jak adnotacje javax.validation czy też @Component , @Service itp. to po prostu twój jezyk / platoforma.

Oczywiście chodzi o język programowania.

Tak, rozumiem że jak piszesz adnotacjami to można powiedzieć że one są "Twoim językiem", ale nie to miałem na myśli.

Miałem na myśli że kiedy korzystasz z rzeczy wbudowanych w język, np String, Integer (takich których nie trzeba doinstalowywać osobno), to zazwyzcaj nie trzeba się od nich oddzielać (chociaż są przypadki. Np sesje w PHP są wbudowane, ale są okropne, i od nich akurat warto się oddzielić).

Gdzie leży granica - sam możesz ją sobie wybrać - które elementy od siebie chciałbyś oddzielić a które nie. Jeśli chcesz poznać dokładnie gdzie tą granicę można położyć to polecam: ten filmik. Cały film jest o tym, konkretnie o tej granicy zaczyna się gdzieś w 29:30. Są ludzie którzy oddzielają się od bibilioteki standardowej również. Są ludzie którzy oddzielają się od framework'u do testów. Sam możesz sobie wybrać od czego. Polecam ten filmik który podlinkowałem, wyjaśni Ci o co chodzi z Dependency Inversion dokładnie.

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