Interfejsy nad implementacje

1

Czytam o dobrych praktykach i serio tak programujecie w firmach, ze np w parametrach jako typ podajecie interfejs a nie konkretna klase? Wiem ze to pozwala na latwiejsze rozwijanie programu, ale tez jest zasada YAGNI, ktora mowi zeby nie projektowac tak, zeby myslec o przyszlosci tylko o teraz. Druga sprawa to jak mamy jako typ podana konkretna klase to latwo mozemy do niej przejsc w IDE, a przy interfejsie trzeba duzo szukac.

0

Sam się chętnie dowiem jak to jest w firmach. :)

5

Ja nie dość, że lubię YAGNI to idę nawet dalej - nie tworzę interfejsów, które miałyby tylko jedną produkcyjną implementację. Staram się nie brać pod uwagę ilości testowych implementacji przy decydowaniu się na robienie hierarchii dziedziczenia.

Mam też takie same odczucia jeśli chodzi o nawigację. Stosowanie nadmiarowych interfejsów powoduje tylko zwiększoną konieczność skakania po kodzie.

Mam > 6 lat komercyjnego doświadczenia, jeśli to ma znaczenie.

Ogólnie dla mnie interfejs mający jedną produkcyjną implementację to zwykle przypadek skrajny, niepodyktowany żadnym rozsądnym powodem. No chyba, że jednak jakiś powód jest i ktoś go tu poda.

0
Krwawy Kaczor napisał(a):

nie projektowac tak, zeby myslec o przyszlosci tylko o teraz

Powiedzmy, że trzeba myśleć o jeden krok naprzód, ale nie o dziesięć.

0

@Azarien: Ale to chyba zależy.

Przykład. Pisze soft obsługujący instalację przemysłową. Ona się nie zmieni przez co najmniej 10 lat. Mogę sobie myśleć 1 lub 0 kroków do przodu.

Przykład. Pisze erp dla jakiejś branży. Drugie wdrożenie w bardzo podobnej firmie i 40 procent procesów znacząco się różni. Dwie prawie identyczne fabryki.

Ja nie znam wielu programistów ale większość tych których znam wykazuje raczej małe zainteresowanie strategicznym celem klienta. To skąd mają wiedzieć co będzie w projekcie za 2-3 sprinty?

1

Jak zapewniasz jakąś funkcjonalność to możesz zaznaczyć obsługę danego interfacea, wtedy gdzieś może się to przydać, a ta jedna linijka dużo nie kosztuje chyba, że implementujesz jakiś nowy interface, to możesz się wstrzymać aż taka funkcjonalność będzie potrzebna.

Ale poco np. ktoś ma się uczyć jak iterować po twoim obiekcie jak może iterować tak jak po wszystkich bo będzie implementowany ten sam interface?

0

Ja nie znam wielu programistów ale większość tych których znam wykazuje raczej małe zainteresowanie strategicznym celem klienta. To skąd mają wiedzieć co będzie w projekcie za 2-3 sprinty?

Pracuję w banku i nawet tutaj wymagania zmieniają się czasem z tygodnia na tydzień. Każdy sprint zaczyna się od planowania i dopiero wtedy mniej więcej wiemy co chcemy osiągnąć. Priorytety zadań dynamicznie się zmieniają, także w zależności od tego jak szybko nam się udaje dostarczyć poprzednie funkcjonalności.

Klepanie kodu na zaś ma taką wadę iż nad zadaniem którego implementację ten wyprzedzający kod miał ułatwić może siedzieć ktoś zupełnie inny. Ten ktoś może nie rozumieć (w zasadzie z dużym prawdopodobieństwem nie zrozumie) abstrakcji, które jeszcze nie zostały użyte we właściwy sposób i albo je wywali albo użyje źle. Jednak zgodnie z zasadą YAGNI nie piszemy abstrakcji na zaś, więc nie musimy się martwić, że abstrakcje zostaną źle użyte przez kogoś innego albo że mogą się nie przydać (bo zadanie do którego przygotowaliśmy kod zostanie odłożone w czasie) i przez to tylko przeszkadzać.

Osobiście wolę ryzykować nadmiarowym refaktorem niż nadmiarowym pogmatwaniem. Z drugiej strony jednak ryzykowanie dodatkowym refaktorem zachodzi w obu przypadkach (czy to z pisaniem kodu na zaś czy nie), bo kod wyprzedzający i tak piszemy nie znając dokładnie szczegółów kolejnego zadania, ani problemów wynikłych przy jego implementacji.

Trzeba też odróżnić umiejętność pisania kodu, który się łatwo refaktoryzuje i uogólnia od pisania niepotrzebnych (na daną chwilę) abstrakcji.

1
Krwawy Kaczor napisał(a):

Czytam o dobrych praktykach i serio tak programujecie w firmach, ze np w parametrach jako typ podajecie interfejs a nie konkretna klase?

To zależy, ale zazwyczaj tak, zresztą nawet IDE podpowiada, aby uogólniać typy. Np. jeśli wiem, że interesuje mnie jedynie iterowanie po kolekcji, to nie ma sensu ograniczać metody do pracy na tablicy czy liście.

Wiem ze to pozwala na latwiejsze rozwijanie programu, ale tez jest zasada YAGNI, ktora mowi zeby nie projektowac tak, zeby myslec o przyszlosci tylko o teraz.

No cóż, wszystko wymaga kompromisu.

Druga sprawa to jak mamy jako typ podana konkretna klase to latwo mozemy do niej przejsc w IDE, a przy interfejsie trzeba duzo szukac.

To chyba w notatniku, bo współczesne IDE pozwalają na łatwe przeskoczenie do implementacji danego interfejsu.

Wibowit napisał(a):

Ogólnie dla mnie interfejs mający jedną produkcyjną implementację to zwykle przypadek skrajny, niepodyktowany żadnym rozsądnym powodem. No chyba, że jednak jakiś powód jest i ktoś go tu poda.

Powodów może być wiele, ja znam (m.in. z tego forum) np. takie:

  • "w książce było napisane, żeby oddzielać abstrakcję od implementacji"
  • "bo inaczej nie można napisać testów jednostkowych"
  • "bo kontener IoC tego wymaga"

Ale to wszystko bzdury. Za to rzeczywistym powodem może być klient zewnętrznego źródła danych. Jeśli nie będzie miał interfejsu, to faktycznie niespecjalnie będzie możliwe pisanie testów jednostkowych dla kodu, który z niego korzysta.

0

"bo kontener IoC tego wymaga"

@somekind to w sumie zależy. Np. w Javie jeśli chcesz skorzystać z AOPa i robisz proxy do klasy, to ta klasa musi być albo niefinalna albo musisz wstrzykiwać interface. Dzięki zastosowaniu interfejsu moge zrobić klase oznaczoną jako final :)

0

Ja zazwyczaj tworzę w osobej gałęzi abstrakcje logiki i w osobnej logike. W abstrakcji opisuje nawet wyjątki czyli pełny dostęp do logiki. Łatwiej zacząc z palca robić xD
O C# traktuję.

A i przepraszam moderatora ale jeszcze dodam że klasa nie interfejs ze względu na kontrolę nad typami xD

0

Interfejs to też rodzaj dodatkowego kwalifikatora dostępu.
Jeśli masz zamknięty projekt w jednym pakiecie to nie ma to specjalnego znaczenia, ale jeśli udostępniasz obiekty z biblioteki poprzez API to warto to API ograniczyć tak mocno jak się da.
Uważam, że układ MyInterface + MyInterfaceImpl jest anty-wzorcem i od razu sugeruje że interfejs jest niepotrzebny.

W testach powinno się testować co najwyżej metody publiczne, więc nie powinno być różnicy (przynajmniej w Javie gdzie nie trzeba dawać virtual) czy coś jest klasą czy interfejsem - o ile da się wstrzyknąć.

Podsumowując: jest to dobra zasada dla początkujących. Jak jesteś krok dalej to zaczynasz dostrzegać, że "to zależy".

0

Interfejsy pozwalają też nieco odwrócić zależności. Jeżeli w moim kodzie biznesowym istnieje zależność do zewnętrznego świata to robię do tego interfejs - np. do bazy danych lub jeżeli wołam inny serwis (aplikację np. po http). Generalnie mam wtedy często jedną implementację (plus ewentualnie jakaś trywialna implementacja in-memory do testowania), ale chcę ograniczyć powiązanie mojego kodu biznesowego z kodem typowo infrastrukturalnym

0

@hcubyc:
Dopóki masz jedną produkcyjną implementację interfejsu to i tak faktycznie masz takie samo powiązanie jakby tego interfejsu nie było.

2

Jest w tym trochę prawdy niestety, ale na moim poziomie mi to pomaga. Tworząc kod, najpierw robię silnę powiązanie między klasą biznesową, a klasą która gdzieś tam sięga po jakieś infrastrukturalne rzeczy, a potem wyciągam z tego interfejs tak żeby pasowało do biznesu i operowało na kodzie biznesowym, by nie mieszać szczegółów implementacyjnych tego kodu infrastrukturalnego do kodu biznesowego. Interfejs w tym kroku mógłby zostać pominięty, bo można by od razu napisać tak klasę od infrastruktury. To co dostaję później to kod biznesowy z zależnością do interfejsu, który mówi tym samym językiem i kod infrastrukturalny, który obsługuje to co ma obsłużyć i nie wyciekają do niego szczegóły biznesowe plus mały fragment kodu który to tłumaczy na wyzej wspomniany interfejs. Dzieki temu mam gotowy klocek np. konsumujący serwis RESTowy nie powiązany z moim biznesem i mogę go później użyć ponownie i kod biznesowy nie powiązany silnie z infrastruktura, a np wydzielajac fragment biznesowy jako osobny moduł/serwis nie muszę rozdzielać infry od biznesu i mogę wybrać inną technologię, bo testy przecież już mam. Nie chcę twierdzić, że to złoty środek i wszyscy powinni tak robić - poprzednio pisałem kod, który był silnie powiązany z infrastrukturą i generalnie nie wyszło to na dobre, teraz rozdzielam to i póki co jest OK, chociaż jest trochę tej pracy dla kroku naprzód, który może nie nastąpić, może za jakiś czas wrócę do pierwszego podejścia i zrobię to lepiej albo spróbuję czegoś innego

1

To co dostaję później to kod biznesowy z zależnością do interfejsu, który mówi tym samym językiem i kod infrastrukturalny, który obsługuje to co ma obsłużyć i nie wyciekają do niego szczegóły biznesowe plus mały fragment kodu który to tłumaczy na wyzej wspomniany interfejs.

Dlaczego jest potrzebny dodatkowy fragment kodu, by tłumaczyć coś na interfejs? Przecież interfejs ma te same metody co klasa implementująca, po co tu coś tłumaczyć? I w jaki sposób wydzielenie interfejsu z klasy ma sprawić, że nie będą z niej wyciekać szczegóły biznesowe?

1

Napisałem sobie przykład, na którym to tłumaczę, ale po krótszym zastanowieniu interfejs równie dobrze mógłbym zastąpić klasą konkretną, jedyna przewaga interfejsu to łatwiejsze mockowanie, bo mockuje obiekt domenowy, a nie kod infrastrukturalny. Jednak nie zmienia to faktu, że mój argument o odwracaniu zależności był inwalidą - chyba, że uznać za wartośc fakt, że wyciągasz kod biznesowy i możesz go spakować gdziekolwiek indziej - mając interfejs, kod się kompiluje, brakuje implementacji, ale to nie problem bo przecież testy określają jak ma działać. Dodatkowy argument, który mi przychodzi na myśl to interfejs robię publiczny, tak by kod spoza mojego pakietu mógł go zaimplementować, klas nie robię publicznych (pozą jedną, która jest punktem wejścia do pakietu), ale to kwestia konwencji

1
Wibowit napisał(a):

To co dostaję później to kod biznesowy z zależnością do interfejsu, który mówi tym samym językiem i kod infrastrukturalny, który obsługuje to co ma obsłużyć i nie wyciekają do niego szczegóły biznesowe plus mały fragment kodu który to tłumaczy na wyzej wspomniany interfejs.

Dlaczego jest potrzebny dodatkowy fragment kodu, by tłumaczyć coś na interfejs? Przecież interfejs ma te same metody co klasa implementująca, po co tu coś tłumaczyć? I w jaki sposób wydzielenie interfejsu z klasy ma sprawić, że nie będą z niej wyciekać szczegóły biznesowe?

Jeśli piszesz jaką bibliotekę która będzie wykorzystywana kilku projektach i jest wymaganie że macie dla produktu X obsłużyć funkcjonalność Y to nawet jeśli wiesz że na 99.9% tylko X będzie z tego korzystać. To i tak lepiej wystawić interfejs niż wymuszać jakieś magiczne zależności od zewnętrznych modółów.

1
  1. Ile zespołów programistów pisze biblioteki, które są używane przez inne zespoły? Moim zdaniem bardzo mało. Dopóki nie piszesz biblioteki to tego nie udawaj. YAGNI.
  2. Jeśli pisałbym publiczną bibliotekę to oczywiście zasady są wtedy inne i interfejs z jedną implementacją (np przykładową) może mieć sens.
  3. Nawet pisząc bibliotekę interfejs nie zawsze ma sens, patrz np: metoda szablonowa (wzorzec projektowy)
0
Wibowit napisał(a):
  1. Ile zespołów programistów pisze biblioteki, które są używane przez inne zespoły? Moim zdaniem bardzo mało. Dopóki nie piszesz biblioteki to tego nie udawaj. YAGNI.

To prawda, ale zmieniają sie frameworki, zespoły. Jeżeli kiedyś przyjdzie do 'odświeżenia' projektu np większej zmiany technologii lub będzie trzeba wydzielić część do np osobnego mikroserwisu to łatwiej mając już do tego podkład niż robiąc to wszystko na raz, potem może się okazać, że jednak projekt się do niczego nie nadaje i trzeba przepisać.

0

Wydzielenie interfejsu z klasy to refaktor jak każdy inny. Jest to nawet prostsze niż np podzielenie klasy na dwie. W IntelliJu wydzielenie interfejsu razem z zastąpieniem użyć klasy użyciami interfejsu to kilka kliknięć: https://www.jetbrains.com/help/idea/extract-interface.html Inne sensowne IDE pewnie też mają taką funkcjonalność.

W jaki sposób przedwczesne wydzielenie interfejsu miałoby w czymś pomóc? Podaj przykład.

0
  1. Ile zespołów programistów pisze biblioteki, które są używane przez inne zespoły? Moim zdaniem bardzo mało. Dopóki nie piszesz biblioteki to tego nie udawaj.

Pisze ficzery do biblioteki, firma w której pracuje się z tego się utrzymuje i większość zatrudnionych albo pisze biblioteki albo je testuje albo zajmuje się utrzymaniem i integracjąe z produktami.

  1. Nawet pisząc bibliotekę interfejs nie zawsze ma sens, patrz np: metoda szablonowa (wzorzec projektowy)

Tak ale wszytko zależy od sytuacji. ale jeśli dane które potrzebujesz są względnie proste(co nie znaczy ze ich uzyskanie też takie jest), to interfejs jest lepszy od wydmuszki od klasy abstrakcyjnej i dziedziczenie po interfejsie nie przysparza tyle problemów co dziedziczenie po klasie.

0

No to masz dość oryginalny projekt w takim razie. Pisanie publicznej biblioteki pociąga za sobą jednak o wiele więcej odpowiedzialności niż wybór pomiędzy wydzielaniem interfejsu lub nie. Pisząc publiczną bibliotekę trzeba:

  • dbać o stabilne i dobrze udokumentowane publiczne API
  • dbać o wsteczną kompatybilność binarną
  • unikać zależności w projekcie, by nie wpędzić użytkowników biblioteki w piekło zależności (dependency hell)
  • dbać o sensowne i konfigurowalne raportowanie błędów
  • itd

Innymi słowy - pisanie publicznej biblioteki (w przeciwieństwie do pisania samodzielnego systemu) ogranicza swobodę. A skoro ogranicza swobodę to i warto czasem robić coś na zaś.

0
Wibowit napisał(a):

Wydzielenie interfejsu z klasy to refaktor jak każdy inny. Jest to nawet prostsze niż np podzielenie klasy na dwie. W IntelliJu wydzielenie interfejsu razem z zastąpieniem użyć klasy użyciami interfejsu to kilka kliknięć: https://www.jetbrains.com/help/idea/extract-interface.html Inne sensowne IDE pewnie też mają taką funkcjonalność.

W jaki sposób przedwczesne wydzielenie interfejsu miałoby w czymś pomóc? Podaj przykład.

W moim kodzie biznesowym potrzebuję produktu, który pobieram z innego serwisu np. po HTTP. Inny serwis ma inną inną domenę, więc ich produkt, który otrzymuje to coś wiecej niż potrzebuję. W tym celu mam mój kodzik i interfejs - one operują całkowicie w mojej domenie. Następnie mam klasę/moduł/whatever klienta tego drugiego serwisu - posługuje się tymi samymi obiektami, co z serwis z którego pobieram dane, korzysta z jego nazewnictwa/konwencji i nic nie wie o moim kodzie biznesowym. Do tego dochodzi implementacja interfesju, która deleguje do klienta drugiego serwisu żadanie pobrania produktów i przetłumaczenie odpowiedzi na moją domenę. Kod domenowy i infrastruktury są oddzielone nie licząc klasy, która tłumaczy/przetwarza to co zwrócił drugi serwis. Jeżeli kiedykolwiek będę musiał w innym projekcie odwołać się do tego drugiego serwisu to już mam gotowego klienta. Gdy nagle ktoś stwierdzi, że trzeba zmienić cały stos technologiczny to może wziąć kod domenowy i powinien się kompilować, trzeba będzie dostować nowy stos technologiczny do istniejących interfejsów, ale testy(specyfikacje) już tam są.

0

Jeśli mamy projekt A który korzysta z projektu B za pomocą HTTP to:

  • mamy klasy DTO używane w projekcie A i B, te klasy są wspólne
  • projekt A ma własne klasy biznesowe (niezależne od klas DTO)
  • projekt B ma własne klasy biznesowe (niezależne od klas DTO)
  • projekt A ma własny konwerter między DTO, a klasami biznesowymi
  • projekt B ma własny konwerter między DTO, a klasami biznesowymi

Gdzie te interfejsy miałyby się pojawić?

0

Projekt A ma interfejs operujący na swoich obiektach domenowych/DTO. Gdzieś tam w innym pakiecie/module jest implementacja tego interfejsu, gdzie jest pobranie danych z serwisu B i konwerter jako szczegół implementacyjny. DTO jest wspólne dla A i B, ale o wspólnym DTO wie tylko moduł (w projekcie A) odpowiedzialny za pobranie i przetworzenie danych. Struktura danych z B nie wycieka do A, chyba że obydwa projekty potrzebują tych samych danych.

0

W sensie nie masz oddzielnych niezależnych klas domenowych i DTO? Jeśli tak to kiepsko. DTO, jak sama nazwa wskazuje, służy tylko i wyłącznie do transferowania danych, a nie wykorzystywania w logice biznesowej.

DTO powinny być maksymalnie niezależne (a więc być osobnymi klasami) od klas domenowych po to by mogły niezależnie ewoluować.

0

Mam, ale struktura danych, którą wykorzystuje jest inna niż to co zwraca drugi projekt. Przykład trochę z czapy, ale nie wymyśliłem lepszego: w domenie interfejs operuje na produkcie, inny serwis zwraca dane dotyczące książek. W warstwie infrastruktury przerabiam DTO książki na DTO produktu. Kod domenowy jest świadomy tylko istnienia zewnętrznej zależności, która dostarcza konkretne produkty. Bez interfejsu nie oddzieliłbym kodu wysłania żadąnia do konkretnego klienta i konwertera z kodu domeny

0

Przykład rzeczywiście z czapy, ale nawet jeśli weźmiemy go pod uwagę to nie możesz po prostu zrobić konwertera z KsiążkaDto bezpośrednio na Produkt (klasa domenowa)? Konwerter nie musi zawsze konwertować 1:1. Sensem istnienia konwertera jest właśnie to, że konwertuje między klasami, które mogą się trochę różnić strukturą (bo przecież te klasy niezależnie ewoluują, o czym pisałem w poprzednim poście).

0

Tu jest trochę pat, bo nie wypuszczam obiektów domenowych poza moduł, czyli w moim podejściu gdybym chciał tak zrobić, że istnieje sobie gdzieś klient do drugiego serwisu i konwerter, który bym chciał zawołać to musiałbym wystawić na świat obiekt domenowy. Z kolei, gdybym miał konwerter w domenie to czymś musiałby być on zasilony, czyli musiałby mieć bezpośrednią zależność do klienta drugiego serwisu, co byłoby mieszaniem kodu z infrastrukturą. Dlatego stosuję taką trochę drogę na około wierząc, że kiedyś to przyniesie to zysk - przyniósł dwa razy, raz faktycznie wydzielając moduł z aplikacji, drugi raz w projekcie prywatnym na boku, gdzie zmieniłem sposób persystencji danych, ale to bylo spowodowane złą decyzją na początku życia projektu. Może i jest to trochę przekombinowane dla większości zależności zewnętrznych, ale w moim odczuciu jest to utrzymywanie 'czystej' domeny

0

Ekhm, no czasami to i metoda Kopiego-Pejsta się sprawdza. Musiałbyś pokazać konkretny przykład na to w jakiej jesteś sytuacji.

Ja lubię podejście mikroserwisów które nie mają dodatkowego przekombinowanego podziału na moduły. Mikroserwisy gadają sobie z użyciem DTO, każdy mikroserwis ma własne obiekty domenowe, które nie muszą być ukrywane pomiędzy modułami i wszystko gra.

Z mojego doświadczenia dokładanie fikuśnej hierarchii modułów do mikroserwisów (które z definicji już mają być małe, więc po co je jeszcze intensywnie ciąć?) to przerost formy nad treścią. Zysku z tego nie ma, a zwykle jest strata, bo mało kto rozumie na czym polega ta fikuśna hierarchia i ostatecznie klasy domenowe lądują w losowych modułach, aby tylko kod się kompilował.

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