Tworzenie nadmiarowych interfejsów.

0

Witam. Mam pytanie dotyczące waszego podejścia w kwestii tworzenia klas oraz interfejsów do nich. Załóżmy, że mamy aplikację napisaną w Javie z użyciem Springa. Często spotykałem się z tym, że dla każdej klasy był tworzony interfejs do niej, nawet jeśli implementowała go tylko ta jedna klasa. Jakie jest wasze podejście w tej kwestii. Załóżmy, że mamy klasę UserSevice, która jest serwisem wykonującym, różne operacje na użytkowniku. Czy tworzycie wtedy do takiej klasy interfejs, nawet jeśli będzie go implementowała tylko jedna klasa? Jak wtedy takie klasy nazywacie? Najczęściej spotkałem się z interfejsem o nazwie UserService a jego implementacją UserServiceImpl. Osobiście nie przekonuje mnie takie podejście jak i wynikając z niego nazewnictwo i moim zdaniem jeśli interfejs ma mieć tylko jedną implementację to nie ma sensu go tworzyć, zwłaszcza, że w Springu, możemy wstrzykiwać po klasie. Moim zdaniem tworzenie interfejsu ma tylko sens jeśli klasa będzie jakimś API, które będzie udostępniane na zewnątrz i z którego będą korzystać inne systemy, lub będzie faktycznie zachodziła relacja dziedziczenia np. GenericDao z kilkoma "gotowymi" metodami i jego implementacje czy interfejs Transfer i kilka implementacji typu: NormalTransfer, ExpressTransfer. Jakie jest wasze podejście w tej kwestii?

4

Powody są 2:

  1. Stare wersje JEE wymagały od enterprise beanów interfejsów, więc trzeba było tak robić.
  2. Jest zasada żeby nie dało się odróżnić implementacji od interfejsu z punktu widzenia użytkownika.

Ad 1) Jeżeli kiedyś cokolwiek trzeba było, a już nie trzeba, to myślę że nie ma sensu brać tego do siebie. Gorsze czasy tutaj raczej nie wrócą.
Ad 2) I tworzenie sztucznego interfejsu łamie ją, bo zwykle robi się IJakiśinterfejs lub JakaśImplementacjaImpl.

Jeżeli ktoś w 2017 roku dalej widzi potrzebę tworzenia interfejsu bo tak, to najpewniej jest dziadkiem który utknął w starych czasach. Jako że taki dziadek jest zwykle seniorem i wyżej, to zwykle ma dużo do powiedzenia w zasadach rozwijania projektu i jest wzorem (heh) dla młodych którzy robią tak samo nie wiedząc dokładnie po co to robią.

2

Również nie jestem zwolennikiem programowania do interfejsów, które są w relacji jeden do jednego z klasą, jeśli nie ma takiej potrzeby, bo jest to zbędna abstrakcja która wprowadza niepotrzebną złożoność.

Natomiast zacznie ciekawsze jest kwestia kiedy interfejsy naprawdę są potrzebne:

  • tak jak już zauważyłeś, na granicach komponentów
  • kiedy potrzebujemy zrobić jakąś bardziej wyszukaną zaślepkę w testach
  • kiedy robimy prawdziwe Dependency inversion & Interface segregation, gdzie to moduły wysokopoziomowy definiuje abstrakcje której potrzebują
  • kiedy potrzebujemy wprowadzić extension point

i pewnie o czymś zapomniałem :)

2

Interfejsy tylko wtedy, gdy jest więcej jak 1 implementacja. Chyba, że trzeba rzeźbić w EJB 3.0. Tam były wymagane, ale to naprawili (w EJB 3.1+).

2

Ja też jestem przeciwnikiem wyodrębniania interfejsów bo tak. Jeśli jest jedna implementacja produkcyjna to interfejsu po prostu nie trzeba. Nawet argument z testowaniem nie jest przekonujący, bo jeśli tworzenie klasy z np nullowymi zależnościami powoduje wysypanie się kodu to prawdopodobnie znaczy, że popełniliśmy błąd i konstruktor robi za dużo: http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/ Pracę potrzebną do zainicjalizowania obiektu (oprócz oczywiście przepisania pól, które trzeba zrobić w środku docelowej klasy) trzeba wydzielić do jakiejś metody fabrycznej lub tego typu osobnego miejsca. Fabrykę wtedy testujemy w innym teście niż tworzoną przez nią klasę.

2
Wibowit napisał(a):

jeśli tworzenie klasy z np nullowymi zależnościami powoduje wysypanie się kodu to prawdopodobnie znaczy, że popełniliśmy błąd i konstruktor robi za dużo

Dlaczego chcesz do konstruktora wpychać nulle? Powinny tam być mocki. Jeśli do konstruktora nie wsadzisz mocków, to będziesz musiał kod testować albo integracyjnie, albo będziesz skazany na wstrzykiwanie zależności w niestandardowy sposób, np. upubliczniając settery, co prowadzi do zepsucia hermetyzacji, albo grzebiąc w prywatnych danych poprzez refleksję (fuj).
Konstruktor powinien doprowadzić obiekt do stanu używalności, żeby nie trzeba było go dodatkowo inicjalizować wywołując jakąś metodę init() czy coś na jej wzór ...Acz w jednej z dyskusji na 4p spotkałem się z podejściem, w którym konstruktor nie robi nic, żeby nie wpłynąć na stan obiektu, ma go tylko stworzyć i już, a faktyczną inicjalizację robi osobna metoda, co jest o tyle problematyczne, że łatwo zapomnieć o wywołaniu dodatkowej metody, a samo podejście pachnie mi sztuką dla sztuki. A więc jeśli obiekt do stanu, w którym można go użyć wymaga wielu operacji, to albo konstruktor musi te operacje przeprowadzić, albo - co moim zdaniem jest sensowniejsze - należy klasę i/lub całą funkcjonalność przeprojektować.
Interfejsy wprowadzają dodatkową warstwę abstrakcji, która w dużych systemach bywa niezbędna, ale w małych faktycznie może tylko zagmatwać kod. Użycie interfejsów wydaje mi się zasadne w momencie użycia kontenera IoC, acz można to opędzić i klasą abstrakcyjną, i zwykłą klasą.

2

Kontener DI nie wymaga interfejsów. Można spokojnie bindować klasę do samej siebie i Wszechświat nie wybucha. A nawet gdyby wymagał to da się napisać jakiegoś kikuta tylko po to by dziedziczyć, a więc napisać klasę abstrakcyjną, która robi wszystko, a potem dopisać klasę dziedziczącą która ma tylko konstruktor delegujący do konstruktora z klasy wyższej.

Konstruktor powinien prawie zawsze robić tylko jedną rzecz - przepisywać parametry do składowych klasy. Tyle. Jeśli wstawię tam nulle (bo nie chce mi się robić mocków czy stubów, a wiem że te zależności nie mogą być użyte w danym teście, więc sens podawania nulli jest) to te nulle będą tylko skopiowane do pól wewnętrznych, a więc nic strasznego nie może się stać. Ergo - tworzenie klasy podając nulle jako parametry powinno co najwyżej stworzyć klasę z niespójnym stanem, ale tworzenie tej klasy powinno się powieść.

Polecam poczytać artykuł Misko Hevery'ego z mojego poprzedniego posta.

1

Ogólnie zgadzam się z przedstawionymi tam przemyśleniami, ale:

Testing Directly is Difficult
Testing such constructors is difficult. To instantiate an object, the constructor must execute. And if that constructor does lots of work, you are forced to do that work when creating the object in tests. If collaborators access external resources (e.g. files, network services, or databases), subtle changes in collaborators may need to be reflected in the constructor, but may be missed due to missing test coverage from tests that weren’t written because the constructor is so difficult to test. We end up in a vicious cycle.

To dotyczy każdej metody, nie tylko konstruktora. Tyle w tym temacie.

It Forces Collaborators on You
Sometimes when you test an object, you don’t want to actually create all of its collaborators. For instance, you don’t want a real MySqlRepository object that talks to the MySql service. However, if they are directly created using new MySqlRepositoryServiceThatTalksToOtherServers() inside your System Under Test (SUT), then you will be forced to use that heavyweight object.

Ktoś tu o DI i mockowaniu nie słyszał (artykuł z 2008 roku!).

It Still is a Flaw even if you have Multiple Constructors (Some for “Test Only”)
Creating a separate “test only” constructor does not solve the problem. The constructors that do work will still be used by other classes.

Tak się po prostu nie robi. Konstruktor specjalnie dla testów oznacza kompletne spieprzenie architektury, ponadto test nie sprawdza wtedy właściwej ścieżki, bo omija stosowany na produkcji konstruktor, a to oznacza dziurę nieprzykrytą testami. Niestety żyjemy w świecie, gdzie utrzymujemy duże, długo rozwijane projekty oparte o rozwiązania, które były projektowane w czasach, kiedy nie było mocków, np. starsze MVC w .net i takie kwiatki się zdarzają, sam kilka popełniłem z całą świadomością ich ułomności.
Co do części The constructors that do work will still be used by other classes., to nieprawda, DI i mocki załatwią sprawę.

Autorzy nie wspomnieli o wydajności testów, a ta jest ważna, jeśli mamy ich tysiące oraz mamy CI i te tysiące testów trwają po kilkadziesiąt minut. Może za tym stać konstruktor, który robi dużo rzeczy, których nie da się zamockować; takie rzeczy są czasochłonne, a to oznacza, że skonstruowanie tylu instancji ile jest testów (i test case'ów) może zająć istotnie długi czas. Czasy testów się wydłużają, nie wystarcza agentów, na których testy są uruchamiane, kolejka rośnie, trzeba czekać... (Testy ofc możesz, a nawet powinieneś odpalić lokalnie przed commitem, ale różnie z tym bywa w praktyce, zwłaszcza jeśli testy trwają bardzo długo).

1

W artykule mocki wspomniane są wielokrotnie. To nie jest artykuł z epoki dinozaurów. EasyMock powstał w roku 2001, więc w momencie pisania artykułu idea automatycznego mockowania w Javie miała co najmniej około 7 lat.

Tak się po prostu nie robi. Konstruktor specjalnie dla testów oznacza kompletne spieprzenie architektury, ponadto test nie sprawdza wtedy właściwej ścieżki, bo omija stosowany na produkcji konstruktor, a to oznacza dziurę nieprzykrytą testami.

No tutaj potwierdzasz słowa Hevery'ego.

Co do części The constructors that do work will still be used by other classes., to nieprawda, DI i mocki załatwią sprawę.

Wyjąłeś to z kontekstu. Hevery mówił o przypadku gdzie robimy konstruktory wykonujące pracę i one chodzą na produkcji oraz obok konstruktory specjalnie do testów. Taka sytuacja jest niedopuszczalna, pisał tak wyraźnie w artykule.

To dotyczy każdej metody, nie tylko konstruktora. Tyle w tym temacie.

Mogę stworzyć obiekt bez wywoływania metody, ale nie mogę wywołać metody bez stworzenia obiektu. Stąd tworzenie obiektu (czyli wywołanie konstruktora) nie może pociągać realnej pracy i w zasadzie o tym jest cały artykuł.

Gwoli wyjaśnienia - constructors that do real work to konstruktory, które łamią zasadę wstrzykiwania zależności. Real work to tutaj tworzenie tychże zależności i (opcjonalnie) zmiana ich stanu.

A z wstrzykiwaniem nulli to chodziło mi o to, że jeśli test sprawdza czy dana zależność ma być nietykana w trakcie tego testu to zamiast robić mocka i potem weryfikować, że nic na nim nie zostało odpalone to mogę wstawić po prostu nulla i jeśli klasa zechce skorzystać z tej zależności to dostaniemy NullPointerException i test się (zgodnie z zamysłem autora) sypnie.

1

Interfejs może mieć taką zaletę, że można go zmokować pisząc implementację manualnie bez używania zewnętrznego frameworka.

1

Klasę też można pod warunkiem, że konstruktor nie robi realnej pracy tylko przepisuje zależności do pól. Oto przykład klasy, w której konstruktor nie wykonuje żadnej realnej pracy oprócz przepisywania zależności do pól (kod w Scali):

class Klaska(zależność1: Zależność1, zależność2: Zależność2) { // Scala daje tutaj fajny cukier składniowy, parametry głównego konstruktora od razu przypisywane są do pól o takich samych nazwach
  def metodka1(): Unit = {
    // tutaj używamy zależność1 i zależność2
  }
}

W testach możemy zrobić sobie ręcznie mocka:

 class MockKlaski(testoweUstrojstwo: TestoweUstrojstwo) extends Klaska(null, null) { // można też wstawić coś innego niż null w razie potrzeby
  override def metodka1(): Unit = {
    // tu możemy robić co się nam żywnie podoba używając np testowego ustrojstwa
  }
}

Jeśli chcemy, by nasza klasa miała zainicjowany i spójny stan to robimy sobie metodę fabrykującą w companion object'u (w Javie byłaby zwykła metoda statyczna):

object Klaska {
  def apply(zależność1: Zależność1): Klaska = {
    zależność1.przygotuj();
    val zależność2 = zróbZależność2()
    new Klaska(zależność1, zależność2)
  }
}

W taki oto sposób mogę sobie instancjonować miliony instancji klasy Klaska, a także dowolnie po niej dziedziczyć w testach zachowując wygodną testowalność.

1
student pro napisał(a):

Interfejs może mieć taką zaletę, że można go zmokować pisząc implementację manualnie bez używania zewnętrznego frameworka.

Nie widzę w tym żadnej zalety. Pisanie do tego własnej implementacji to wyważanie otwartych drzwi. Frameworki do testów jednostkowych i do mocków od lat zawierają praktycznie wszystko, czego trzeba. Pamiętaj, że kod odpowiedzialny za testy nie musi lądować na produkcji, więc tych bibliotek nie trzeba tam dołączać. Pamiętaj, że testy trzeba też utrzymywać, więc nie powinny zawierać kodu, który jest zbędny (chyba, że poprawia to czytelność pewnych fragmentów).
Ale może nie mam racji i czegoś nie widzę, podpowiesz dlaczego widzisz zaletę własnej implementacji mocka?

0

Po prostu mniej dodatkowych bibliotek/zależności, ale to taka dyskusyjna zaleta, nie nalegam ;)

0
Wibowit napisał(a):

Klasę też można pod warunkiem, że konstruktor nie robi realnej pracy tylko przepisuje zależności do pól. (...)

No i pod warunkiem że nie ma problemu z finalami. Ale czemu nie zrobić w takim razie bezargumentowego konstruktora protected?

0
margor90 napisał(a):

Interfejsy tylko wtedy, gdy jest więcej jak 1 implementacja. Chyba, że trzeba rzeźbić w EJB 3.0. Tam były wymagane, ale to naprawili (w EJB 3.1+).

W prostych programach tak.

Ale w bardziej złożonych skąd wiesz ile będzie potrzebnych implementacji danej klasy ?

Czy za 2 lata lub 5 lat dalej będzie potrzebna jedna implementacja ?

Interfejsy są wybieganiem w przyszłość, gwarancją, że nie trzeba będzie przebijać się przez miliony linii kodu w celu zmiany referencji do tej jednej klasy.

1

@Bogaty Terrorysta Zdajesz sobie sprawę że zrobienie tego w nowoczesnym IDE to kilka kliknięć? A dodawanie rzeczy których nie potrzebujesz tylko zaśmieca kod.

2

Dawno temu miałem kolegę, który do klas w Javie dorzucał pola

private Object reserved1;  //na wypadek gdyby kiedyś były potrzebne 
private Object reserved2;  //reserved for future use
private Object reserved3; 

(Przy okazji ten koncept pochodzi z języka C (z bibliotek) i tam miał trochę sensu.

Dodawanie interfejsów, bo kiedyś możesz mieć inną implementację, ma mniej więcej taki sam sens jak reserved1 w javie.

2

Interfejsy służą do definiowania protokołów komunikacji pomiędzy klasami. I są w tym zdecydowanie lepsze niż bezpośrednio używane klasy. Można bez nich się obejść, ale tylko wtedy, gdy nie chcemy gadać z elementami spoza naszego podwórka. Przykładowo w nie publicznych klasach wewnętrznych.

Tworzenie obiektów

Konstruktor powinien przyjmować jako parametry wszystkie wymagane „podzespoły”. Inaczej mówiąc, wprost definiujemy wymagane zależności na poziomie semantycznym. Wtedy nie ma potrzeby używania DI. DI będzie tylko narzędziem ułatwiającym życie. Dlaczego zatem interfejsy? Ponieważ tworząc obiekt, nie powinniśmy wiązać się z implementacją jego zależności. Jeżeli tworzę jakąś zależność, to dobrze jest, gdy zdefiniujemy wspólny sposób komunikacji. W przeciwnym wypadku jakakolwiek zmiana w zależności, jak i zależnościach tranzytywnych, może okazać się zabójcza dla naszego kodu.

Testowanie i mockowanie

Jeżeli mogę utworzyć obiekt w jedyny „słuszny” sposób, to przystępując do testowania, wiem, czego potrzebuję. Jeżeli opieram zależności o interfejsy, to znacznie łatwiej jest mi zarządzać testem. W dodatku mogę wtedy skupić się na testowaniu mojego kodu, a nie pomaganiu Szczepanowi Faberowi w testowaniu Mockito. Dobrze opisuje to Jose Valim. W przypadku tego typu podejścia nie musimy zwracać uwagi na elementy finalne. W dodatku jawna deklaracja wszystkich zależności pozwala na uniknięcie zależności statycznych.

Techniki

Wbrew pozorom tworzenie interfejsów nie jest aż tak czasochłonne. Idea, Eclipse, Netbeans posiadają odpowiednie refaktoryzacje w rodzaju „extract interface”. Jeżeli wykorzystujemy TDD w naszej pracy, to bardzo naturalną metodą pracy jest zdefiniowanie interfejsu. Podobnie ma się rzecz w przypadku projektowania zgodnie z DDD.

Jeszcze inną metodą jest tworzenie kodu zgodnie z zasadami Precision Parameter Passing:

Pozwala ona na komponowanie serwisów tak, by minimalizować ryzyko błędów. Zresztą tę technikę wykorzystuje się w czasie pracy z AngularJS, gdzie do serwisów jawnie przekazujemy WSZYSTKIE zależności.

0

@Koziołek:
Zgadzam się że sam interfejs jest ważny i to jego zawartość powinna być istotna a nie sama implementacja.
Uważam jednak że definiując klasę, ona sama z siebie posiada pewien interfejs, a dokładniej nazwę i metody publiczne. Jeżeli w IDE zwiniemy klamry to widok będzie podobny do tego z interfejsu. Dlatego pisząc klasę, da się określić jej interfejs bez wystawiania go jawnie. Ktoś kto użyje klasy czy to bezpośrednio czy to przez interfejs nie powinien w żadnym wypadku widzieć różnicy. Dlatego stworzenie interfejsu do 1 klasy jest dla mnie po prostu nadmiarowym opisem. Skoro dobre IDE potrafi zrobić to sam po wybraniu odpowiedniej opcji to znaczy że jest do akcja deterministyczna i posiadasz wszystkie informacje do jej wykonania, czyli nadmiarowa.

0

A ja myślałem że chodzi o łatwe stworzenie proxy. Ale może się mylę :D

2

Ktoś kto użyje klasy czy to bezpośrednio czy to przez interfejs nie powinien w żadnym wypadku widzieć różnicy.

@krzysiek050: No nie do końca, ponieważ używając interfejsu interesują mnie tylko pewne zachowania, które są określone w tym interfejsie. Używając klasy mam dostęp do wszystkich zachowań. Świetnie widać, to w przypadku Springa i hierarchii CRUD Dao. Możesz na poziomie serwisu napisać, że chcesz CrudDao i mieć dostęp tylko do podstawowych operacji. Możesz powiedzieć, że chcesz konkretne Dao i mieć dostęp do specyficznych mechanizmów takiego interfejsu.

0

@Koziołek
To już głębszy przykład, bo takie coś ma miejsce jeżeli jest wiele implementacji. Jeżeli jest jedna, to możesz co najwyżej rozbić zachowania klasy na kilka interfejsów które ograniczą część jej możliwości. Ale jeżeli jedna klasa ma kilka interfejsów z których tylko ona używa, to znaczy że dałoby się ją rozbić na mniejsze części i powstałyby pary 1 interfejs - 1 klasa, czyli wracamy do punktu wyjścia.

0

TL;DR

Robienie do wszystkiego interfejsów może być trochę durne (zwłaszcza jak jest jedna implementacja).
Proponuję prostą zasadę: interfejs do wszystkiego co wstrzykujemy (ale dzisiaj, a nie co moglibyśmy ew. wstrzykiwać).
A klasy abstrakcyjne tylko do dziedziczenia - o ile mają jakąś funkcjonalność.

Zalety interfejsu w stosunku do klasy abstrakcyjnej:

  • można implementować wiele interfejsów, dziedziczyć można tylko z jednej klasy
  • klasa abstrakcyjna może mieć więcej niż byśmy chcieli przekazać do klienta - np. zarządzanie instancją, konstruktory, atrybuty
  • interfejsu nie można rozszerzyć przez proste dopisanie słówka "public" przed istniejącą metodą
2

Proponuję prostą zasadę: interfejs do wszystkiego co wstrzykujemy (ale dzisiaj, a nie co moglibyśmy ew. wstrzykiwać).

Proponuję tego nie robić. Jest jedna implementacja i nie jest to nasze API - no to nie bawimy się w interfejsy.

http://www.adam-bien.com/roller/abien/entry/rethinking_packaging_modularization_interfaces_with

(Pomijając, że proponuję najlepiej niczego nie wstrzykiwać - a tylko zwyczajnie instancjonować - jak ludzie).

0
jarekr000000 napisał(a):

(Pomijając, że proponuję najlepiej niczego nie wstrzykiwać - a tylko zwyczajnie instancjonować - jak ludzie).

Czyli masz trzy klasy/interfejsy wymagane przez konstruktor, każda z nich wymaga do stworzenia kolejnych trzech klas/interfejsów, dziergasz te dziewięć new w każdym miejscu, w którym potrzebujesz instancję. A pół roku później robisz refactoring i z pierwszy z parametrów zmieniasz na inny interfejs, ponadto dodajesz czwarty, następnie spędzasz cały dzień na poprawianiu miejsc, w których tworzysz instancję. Brawo! Wykonałeś z wielkim sukcesem masę nikomu niepotrzebnej pracy. A teraz siadasz do aktualizowania testów...

1

Jakoś dziwnym trafem jedyne problemy z systemem po refaktoringu i z dopasowaniem testów widzę w systemach grubo korzystających z JavaEE lub Spring - bo się np. nie te co trzeba zapomniane implementacje wstrzykują i po zmianie mocki siesypią.
A tak btw. jak byś się dobrze zastanowił to nie ma zwykle 9 new. ( Czasem mam przez chwilę i to oznaka, że słabo zaprojektowałem system :-) Właśnie jeden taki case poprawiam :-) )
Normalnie nie przekazujesz wszystkich zależności w głąb. (a raczej jedną).

1

Jakoś dziwnym trafem jedyne problemy z systemem po refaktoringu i z dopasowaniem testów widzę w systemach grubo korzystających z JavaEE lub Spring

@jarekr000000 nic w tym dziwnego, bo to są pewnie zwyczajnie duze systemy. W kodzie na 200 linijek w Haskellu tego problemu nie będzie, nie dlatego ze Haskell taki dobry tylko ze złożoność problemu mniejsza. Takie podejście że wstrzykiwanie jest złe jest równie krókowzroczne co podejście new jest złe ;)

1

Tylko widzisz ja np. te "duże Systemy" powoli przerabiam na normalne. I jakoś im się poprawia. (Co prawda wywalanie niepotrzebnego wstrzykiwania to tylko część pracy i raczej nie najważniejsza).

0

Co różni "duży system" od "normalnego"? Jak sprawdzasz/mierzysz to, że systemowi się "poprawiło"?
Usuwanie DI pachnie premature optimization. Jeśli IoC jest za ciężkie, to trzeba użyć lżejszego frameworka, a nie poświęcać czas na zmienianie w kilkuset miejscach kodu, po których to zamianach zyska się jeden promil na wydajności. Optymalizuje się tylko wąskie gardła.

0

@jarekr000000:
czyli robisz jakoś tak?:

class Root{
    Root(){
        this.a = new A( new B( new C( new D())));
    }
}

To chyba lepiej robić new w konstruktorach i dodać setery, w końcu setery to też forma wstrzykiwania zależności ;)

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