Czy muszę używać interfejsu, gdy tylko jedna klasa go zaimplementuje?

242

Czy nie chodzi o to, że wiele klas przestrzega zestawu reguł i implementacji?

Lamin Sanneh
źródło
16
Lub, aby ułatwić nieprzystosowanie.
11
Zezwolenie wielu klasom na implementację interfejsów i uzależnienie kodu od interfejsów jest NIEZBĘDNE w izolacji do testów jednostkowych. Jeśli wykonujesz testy jednostkowe, będziesz mieć inną klasę implementującą ten interfejs.
StuperUser,
2
Reddit dyskusja na ten temat.
yannis,
6
Pola i metody publiczne są same w sobie „interfejsem”. Jeśli brak polimorfizmu jest celowo zaplanowany, nie ma powodu, aby używać interfejsu. Jednostka testująca inne wymienione to planowane zastosowanie polimorfizmu.
mike30

Odpowiedzi:

204

Ściśle mówiąc, nie, nie, obowiązuje YAGNI . To powiedziawszy, czas spędzony na tworzeniu interfejsu jest minimalny, szczególnie jeśli masz przydatne narzędzie do generowania kodu, które wykonuje większość pracy za Ciebie. Jeśli nie masz pewności, czy będziesz potrzebować interfejsu, czy nie, powiedziałbym, że lepiej pomylić się w kwestii obsługi definicji interfejsu.

Ponadto użycie interfejsu nawet dla jednej klasy zapewni kolejną próbną implementację testów jednostkowych, która nie jest produkowana. Odpowiedź Avnera Shahara-Kashtana rozwija się w tym punkcie.

Yannis
źródło
84
Testowanie +1 oznacza, że ​​prawie zawsze masz dwie implementacje
jk.
24
@YannisRizos Nie zgadzam się z tym ostatnim punktem z powodu Yagni. Rozruch interfejsu z metod publicznych klas jest trywialny po fakcie, podobnie jak zamiana CFoo na IFoo w konsumpcji klas. Nie ma sensu pisać tego z wyprzedzeniem.
Dan Neely,
5
Nadal nie jestem pewien, czy podążam za tobą. Ponieważ narzędzia do generowania kodu sprawiają, że dodawanie go po fakcie jest jeszcze tańsze, nie widzę powodu, aby tworzyć interfejs, zanim będzie to wyraźnie potrzebne.
Dan Neely,
6
Myślę, że brakujący interfejs nie jest dobrym przykładem dla YAGNI, ale dla „uszkodzonego okna” i brakującej dokumentacji. Użytkownicy klasy są praktycznie zmuszeni do kodowania implementacji zamiast abstrakcji, jak powinni.
Fabio Fracassi
10
Dlaczego miałbyś kiedykolwiek zanieczyszczać swoją bazę kodu bezsensownym cruftem tylko po to, aby spełnić niektóre ramy testowe? Mówię poważnie, jestem po prostu samoukiem po stronie klienta JavaScript, który próbuje uporządkować WTF, myli się z implementacjami COD i OOD dla programistów Java, z którymi ciągle się spotykam, dążąc do tego, by stać się bardziej wszechstronnym generalistą, ale dlaczego nie „ Czy wyrzucają IDE z twoich rąk i nie pozwalają ci go odzyskać, dopóki nie nauczysz się, jak pisać czysty, czytelny kod, kiedy przyłapią cię na robieniu czegoś takiego na studiach? To po prostu nieprzyzwoite.
Erik Reppen
144

Odpowiedziałbym, że to, czy potrzebujesz interfejsu, czy nie, nie zależy od tego, ile klas go zaimplementuje. Interfejsy to narzędzie do definiowania umów między wieloma podsystemami aplikacji; więc tak naprawdę liczy się podział aplikacji na podsystemy. Powinny istnieć interfejsy jako interfejs do enkapsulowanych podsystemów, bez względu na to, ile klas je implementuje.

Oto jedna bardzo przydatna zasada:

  • Jeśli sprawisz, że klasa będzie Fooodnosić się bezpośrednio do klasy BarImpl, zdecydowanie zobowiązujesz się do zmiany za Fookażdym razem, gdy ją zmieniasz BarImpl. Zasadniczo traktujesz je jako jedną jednostkę kodu podzieloną na dwie klasy.
  • Jeśli sprawisz, że Fooodwołasz się do interfejsu Bar , zobowiązujesz się do tego, aby uniknąć zmian w Foomomencie zmiany BarImpl.

Jeśli zdefiniujesz interfejsy w kluczowych punktach aplikacji, dokładnie przemyślisz metody, które powinny one obsługiwać, a które nie powinny, i komentujesz interfejsy, aby opisać, jak powinna się zachowywać implementacja (a jak nie), Twoja aplikacja będzie znacznie łatwiejsza do zrozumienia, ponieważ te komentowane interfejsy zapewnią rodzaj specyfikacji aplikacji - opis tego, jak ma się zachowywać. To znacznie ułatwia odczytanie kodu (zamiast pytać „co do cholery ma zrobić ten kod”, możesz zapytać „jak ten kod robi to, co powinien”).

Oprócz tego wszystkiego (lub właśnie z tego powodu) interfejsy promują osobną kompilację. Ponieważ interfejsy są trywialne w kompilacji i mają mniej zależności niż ich implementacje, oznacza to, że jeśli napiszesz klasę w Foocelu użycia interfejsu Bar, zwykle można dokonać ponownej kompilacji BarImplbez konieczności ponownej kompilacji Foo. W dużych aplikacjach może to zaoszczędzić dużo czasu.

sacundim
źródło
14
Głosowałbym za tym wiele razy, gdybym mógł. IMO najlepsza odpowiedź na to pytanie.
Fabio Fracassi
4
Jeśli klasa pełni tylko jedną rolę (tzn. Miałaby zdefiniowany tylko jeden interfejs), to dlaczego nie zrobić tego za pomocą metod publicznych / prywatnych?
Matthew Finlay,
4
1. Organizacja kodeksu; posiadanie interfejsu we własnym pliku, który ma tylko podpisy i komentarze do dokumentacji, pomaga utrzymać kod w czystości. 2. Szkielety, które zmuszają cię do ujawnienia metod, które wolisz zachować prywatność (np. Kontenery wstrzykujące zależności za pośrednictwem publicznych zestawów ustawień). 3. Oddzielna kompilacja, jak już wspomniano; jeśli klasa Foozależy od interfejsu Barnastępnie BarImplmogą być modyfikowane bez konieczności rekompilacji Foo. 4. Bardziej szczegółowa kontrola dostępu niż oferty publiczne / prywatne (narażaj tę samą klasę na dwóch klientów za pośrednictwem różnych interfejsów).
sacundim
3
I na koniec: 5. Klienci (najlepiej) nie powinni obchodzić się, ile klas ma mój moduł, ile, a nawet nie są w stanie powiedzieć. Wszystko, co powinni zobaczyć, to zestaw typów oraz niektóre fabryki lub fasady, które pozwolą uruchomić piłkę. Tzn. Często widzę wartość w enkapsulacji nawet tego, z jakich klas składa się twoja biblioteka.
sacundim,
3
@sacundim If you make class Foo refer directly to class BarImpl, you're strongly committing yourself to change Foo every time you change BarImplPo zmianie BarImpl, jakich zmian można uniknąć w Foo za pomocą interface Bar? Ponieważ podpisy i funkcjonalność metody nie zmieniają się w BarImpl, Foo nie będzie wymagało zmian nawet bez interfejsu (cały cel interfejsu). Mówię o scenariuszu, w którym tylko jedna klasa, tj. BarImplImplementuje Bar. W scenariuszu z wieloma klasami rozumiem zasadę odwrócenia zależności i to, jak interfejs jest pomocny.
Shishir Gupta
101

Interfejsy wyznaczono w celu zdefiniowania zachowania, tj. Zestawu prototypów funkcji / metod. Typy implementujące interfejs zaimplementują to zachowanie, więc kiedy masz do czynienia z takim typem, wiesz (częściowo) jakie ma ono zachowanie.

Nie ma potrzeby definiowania interfejsu, jeśli wiesz, że zdefiniowane przez niego zachowanie zostanie użyte tylko raz. KISS (niech to będzie proste, głupie)

superM
źródło
Nie zawsze możesz użyć interfejsu w jednej klasie, jeśli jest to klasa ogólna.
John Isaiah Carmona,
Co więcej, możesz wprowadzić interfejs, gdy będzie on ostatecznie potrzebny w nowoczesnym środowisku IDE, dzięki refaktoryzacji „interfejsu wyodrębniania”.
Hans-Peter Störr,
Rozważmy klasę użyteczności, w której istnieją tylko publiczne metody statyczne oraz prywatny konstruktor - całkowicie akceptowalne i promowane przez autorów takich jak Joshua Bloch i in.
Darrell Teague,
63

Chociaż teoretycznie nie powinieneś mieć interfejsu tylko ze względu na interfejs, odpowiedź Yannisa Rizosa wskazuje na dalsze komplikacje:

Kiedy piszesz testy jednostkowe i używasz fałszywych frameworków, takich jak Moq lub FakeItEasy (aby wymienić dwa najnowsze, których użyłem), domyślnie tworzysz inną klasę, która implementuje interfejs. Przeszukiwanie kodu lub analiza statyczna może wskazywać, że istnieje tylko jedna implementacja, ale w rzeczywistości istnieje wewnętrzna próbna implementacja. Za każdym razem, gdy zaczynasz pisać makiety, odkrywasz, że ekstrakcja interfejsów ma sens.

Ale czekaj, jest więcej. Istnieje więcej scenariuszy, w których istnieją niejawne implementacje interfejsu. Na przykład za pomocą stosu komunikacyjnego WCF platformy .NET generuje serwer proxy do usługi zdalnej, która ponownie implementuje interfejs.

W czystym środowisku kodu zgadzam się z resztą odpowiedzi tutaj. Należy jednak zwrócić uwagę na wszelkie struktury, wzorce lub zależności, które mogą korzystać z interfejsów.

Avner Shahar-Kashtan
źródło
2
+1: Nie jestem pewien, czy YAGNI ma tutaj zastosowanie, ponieważ posiadanie interfejsu i korzystanie z frameworków (np. JMock itp.), Które wykorzystują interfejsy, mogą faktycznie zaoszczędzić czas.
Deco
4
@Deco: dobre frameworki (między innymi JMock) nawet nie potrzebują interfejsów.
Michael Borgwardt,
12
Tworzenie interfejsu wyłącznie z powodu ograniczenia frakcji kpina wydaje mi się okropnym powodem. Na przykład korzystanie z EasyMock drwiny z klasy jest tak samo łatwe jak interfejs.
Alb
8
Powiedziałbym, że jest na odwrót. Używanie próbnych obiektów w testach oznacza z definicji tworzenie alternatywnych implementacji interfejsów. Dzieje się tak niezależnie od tego, czy tworzysz własne klasy FakeImplementation, czy pozwalasz sobie na kpiące ramy. Mogą istnieć pewne frameworki, takie jak EasyMock, które wykorzystują różne hacki i sztuczki niskiego poziomu, aby wyśmiewać konkretne klasy - i więcej mocy dla nich! - ale koncepcyjnie fałszywe obiekty są alternatywnymi realizacjami kontraktu.
Avner Shahar-Kashtan
1
Nie wypuszczasz testów w produkcji. Dlaczego próbna klasa, która nie potrzebowała interfejsu, potrzebuje interfejsu?
Erik Reppen
32

Nie, nie potrzebujesz ich i uważam, że jest to anty-wzorzec do automatycznego tworzenia interfejsów dla każdego odwołania do klasy.

Stworzenie Foo / FooImpl jest naprawdę kosztowne. IDE może utworzyć interfejs / implementację za darmo, ale kiedy nawigujesz po kodzie, masz dodatkowe obciążenie poznawcze z F3 / F12 po foo.doSomething()przeniesieniu cię do podpisu interfejsu, a nie faktycznej implementacji, którą chcesz. Dodatkowo masz bałagan dwóch plików zamiast jednego do wszystkiego.

Więc powinieneś to zrobić tylko wtedy, gdy naprawdę potrzebujesz tego do czegoś.

Teraz odnosząc się do kontrargumentów:

Potrzebuję interfejsów dla platform wstrzykiwania zależności

Interfejsy do obsługi ram są starsze. W Javie interfejsy były wymaganiem dla dynamicznych serwerów proxy, wcześniejszych niż CGLIB. Dzisiaj zwykle tego nie potrzebujesz. Uważany jest za postęp i dar dla produktywności programistów, że nie są już potrzebne w EJB3, Spring itp.

Potrzebuję próbnych testów jednostkowych

Jeśli piszesz własne symulacje i masz dwie rzeczywiste implementacje, odpowiedni jest interfejs. Prawdopodobnie nie prowadzilibyśmy tej dyskusji w pierwszej kolejności, gdyby baza kodów zawierała zarówno FooImpl, jak i TestFoo.

Ale jeśli używasz kpiącego frameworku, takiego jak Moq, EasyMock lub Mockito, możesz kpić z klas i nie potrzebujesz interfejsów. Jest to analogiczne do ustawienia foo.method = mockImplementationw dynamicznym języku, w którym można przypisać metody.

Potrzebujemy interfejsów, aby postępować zgodnie z zasadą inwersji zależności (DIP)

DIP mówi, że budujesz w oparciu o umowy (interfejsy), a nie implementacje. Ale klasa jest już umową i abstrakcją. Po to są słowa kluczowe publiczne / prywatne. Na uniwersytecie kanoniczny przykład był czymś w rodzaju Matrycy lub Wielomianu - konsumenci mają publiczny interfejs API do tworzenia matryc, dodawania ich itp., Ale nie wolno im dbać o to, czy matryca jest implementowana w postaci rzadkiej lub gęstej. Nie było wymagane użycie IMatrix ani MatrixImpl do udowodnienia tego.

Ponadto DIP jest często nadmiernie stosowany na każdym poziomie wywołania klasy / metody, nie tylko na głównych granicach modułu. Znakiem, że nadmiernie stosujesz DIP, jest zmiana interfejsu i implementacji w kroku blokady, tak że musisz dotknąć dwóch plików, aby dokonać zmiany zamiast jednego. Jeśli DIP zostanie zastosowany odpowiednio, oznacza to, że Twój interfejs nie powinien się często zmieniać. Kolejnym znakiem jest to, że twój interfejs ma tylko jednego prawdziwego konsumenta (własną aplikację). Inna historia, jeśli budujesz bibliotekę klas do konsumpcji w wielu różnych aplikacjach.

Jest to następstwo argumentu wuja Boba Martina na temat kpienia - wystarczy kpić z głównych granic architektonicznych. W aplikacji internetowej głównym ograniczeniem są dostęp HTTP i DB. Wszystkie wywołania klasy / metody pomiędzy nimi nie są. To samo dotyczy DIP.

Zobacz też:

wrschneider
źródło
4
Wyśmiewanie klas zamiast interfejsów (przynajmniej w Javie i C #) należy uznać za przestarzałe, ponieważ nie ma sposobu, aby zapobiec uruchomieniu konstruktora nadklasy, co może spowodować, że Twój pozorowany obiekt będzie oddziaływać w środowisku w niezamierzony sposób. Wyśmiewanie interfejsu jest bezpieczniejsze i łatwiejsze, ponieważ nie musisz myśleć o kodzie konstruktora.
Jules
4
Nie napotkałem problemów z drwiącymi klasami, ale frustruje mnie nawigacja IDE, która nie działa zgodnie z oczekiwaniami. Rozwiązanie prawdziwego problemu bije hipotetycznie.
wrschneider,
1
@Jules Możesz wyśmiewać konkretną klasę, w tym jej konstruktor w Javie.
assylias,
1
@assylias jak zapobiec uruchamianiu konstruktora?
Jules
2
@Jules Zależy to od twoich frameworków - na przykład dzięki jmockit możesz po prostu pisać, new Mockup<YourClass>() {}a cała klasa, łącznie z jej konstruktorami, jest wyśmiewana, niezależnie od tego, czy jest to interfejs, klasa abstrakcyjna czy klasa konkretna. Możesz również „zastąpić” zachowanie konstruktora, jeśli chcesz to zrobić. Przypuszczam, że istnieją równoważne sposoby w Mockito lub Powermock.
assylias,
22

Wygląda na to, że odpowiedzi po obu stronach ogrodzenia można podsumować w następujący sposób:

Dobrze projektuj i umieszczaj interfejsy tam, gdzie są one potrzebne.

Jak zauważyłem w odpowiedzi na odpowiedź Yanni , nie sądzę, żebyś kiedykolwiek miał twardą i szybką regułę dotyczącą interfejsów. Reguła musi być z definicji elastyczna. Moją zasadą dotyczącą interfejsów jest to, że interfejs powinien być używany wszędzie tam, gdzie tworzysz API. Interfejs API powinien być tworzony wszędzie tam, gdzie przekraczasz granicę z jednej dziedziny odpowiedzialności do drugiej.

Na przykład (strasznie wymyślony) załóżmy, że budujesz Carklasę. W swojej klasie na pewno potrzebujesz warstwy interfejsu użytkownika. W tym konkretnym przykładzie, przybiera formę IginitionSwitch, SteeringWheel, GearShift, GasPedali BrakePedal. Ponieważ ten samochód zawiera AutomaticTransmission, nie potrzebujesz ClutchPedal. (A ponieważ jest to okropny samochód, nie ma klimatyzacji, radia ani fotela. W rzeczywistości brakuje też desek podłogowych - wystarczy trzymać się kierownicy i mieć nadzieję na najlepsze!)

Która z tych klas potrzebuje interfejsu? Odpowiedzią może być każda z nich lub żadna z nich - w zależności od projektu.

Możesz mieć interfejs wyglądający tak:

Interface ICabin
    Event IgnitionSwitchTurnedOn()
    Event IgnitionSwitchTurnedOff()
    Event BrakePedalPositionChanged(int percent)
    Event GasPedalPositionChanged(int percent)
    Event GearShiftGearChanged(int gearNum)
    Event SteeringWheelTurned(float degree)
End Interface

W tym momencie zachowanie tych klas staje się częścią interfejsu / API ICabin. W tym przykładzie klasy (jeśli istnieją) są prawdopodobnie proste, z kilkoma właściwościami i funkcją lub dwiema. To, co domyślnie twierdzisz w swoim projekcie, polega na tym, że klasy te istnieją wyłącznie w celu wspierania jakiejkolwiek konkretnej implementacji ICabin, którą posiadasz, i nie mogą istnieć same, lub są pozbawione znaczenia poza kontekstem ICabin.

Jest to ten sam powód, dla którego nie testujesz jednostek prywatnych członków - istnieją tylko po to, aby obsługiwać publiczny interfejs API, dlatego ich zachowanie należy przetestować, testując interfejs API.

Jeśli więc twoja klasa istnieje wyłącznie po to, aby obsługiwać inną klasę, a koncepcyjnie postrzegasz ją jako tak naprawdę nieposiadającą własnej domeny , możesz pominąć interfejs. Ale jeśli twoja klasa jest na tyle ważna, że ​​uważasz ją za wystarczająco dorosłą, by mieć własną domenę, to idź i daj jej interfejs.


EDYTOWAĆ:

Często (w tym w tej odpowiedzi) czytasz takie rzeczy jak „domena”, „zależność” (często w połączeniu z „zastrzykiem”), które nic dla ciebie nie znaczą, gdy zaczynasz programować (na pewno nie coś dla mnie znaczy). W przypadku domeny oznacza to dokładnie, jak to brzmi:

Terytorium, na którym sprawowana jest władza lub władza; posiadanie suwerena lub wspólnoty, lub tym podobne. Używany również w przenośni. [WordNet sense 2] [1913 Webster]

Pod względem mojego przykładu - rozważmy IgnitionSwitch. W samochodzie z mięsem wyłącznik zapłonu odpowiada za:

  1. Uwierzytelnianie (brak identyfikacji) użytkownika (potrzebuje poprawnego klucza)
  2. Dostarczenie prądu do rozrusznika, aby mógł faktycznie uruchomić samochód
  3. Dostarczanie prądu do układu zapłonowego, aby mógł on dalej działać
  4. Wyłączam prąd, aby samochód się zatrzymał.
  5. W zależności od tego, jak go widzisz, w większości (wszystkich?) Nowszych samochodów jest przełącznik, który zapobiega wyjęciu kluczyka ze stacyjki, gdy skrzynia biegów jest poza Parkiem, więc może to być część jego domeny. (W rzeczywistości oznacza to, że muszę przemyśleć i przeprojektować mój system ...)

Właściwości te tworzą domenę z poniższych IgnitionSwitch, czyli innymi słowy, co wie na temat i jest odpowiedzialny za.

IgnitionSwitchNie jest odpowiedzialny za GasPedal. Wyłącznik zapłonu jest całkowicie nieświadomy pedału gazu pod każdym względem. Oba działają całkowicie niezależnie od siebie (choć samochód byłby bezwartościowy bez nich obu!).

Jak pierwotnie powiedziałem, zależy to od twojego projektu. Możesz zaprojektować taki, IgnitionSwitchktóry ma dwie wartości: On ( True) i Off ( False). Możesz też zaprojektować go tak, aby uwierzytelniał dostarczony klucz i wiele innych działań. To trudna część bycia deweloperem decyduje, gdzie narysować linie na piasku - i szczerze mówiąc przez większość czasu jest to całkowicie względne. Te linie na piasku są jednak ważne - tam jest twój interfejs API, a zatem i gdzie powinny być twoje interfejsy.

Wayne Werner
źródło
Czy mógłbyś bardziej szczegółowo wyjaśnić, co rozumiesz przez „mieć własną domenę”?
Lamin Sanneh
1
@ LaminSanneh, opracowane. To pomaga?
Wayne Werner,
8

Nie (YAGNI) , chyba że planujesz napisać testy dla innych klas korzystających z tego interfejsu, a testy te przyniosłyby korzyść z wyśmiewania interfejsu.

stijn
źródło
8

Z MSDN :

Interfejsy są lepiej dostosowane do sytuacji, w których aplikacje wymagają wielu potencjalnie niepowiązanych typów obiektów, aby zapewnić określone funkcje.

Interfejsy są bardziej elastyczne niż klasy podstawowe, ponieważ można zdefiniować pojedynczą implementację, która może implementować wiele interfejsów.

Interfejsy są lepsze w sytuacjach, w których nie trzeba dziedziczyć implementacji z klasy podstawowej.

Interfejsy są przydatne w przypadkach, w których nie można używać dziedziczenia klas. Na przykład struktury nie mogą dziedziczyć po klasach, ale mogą implementować interfejsy.

Zasadniczo w przypadku pojedynczej klasy implementacja interfejsu nie będzie konieczna, ale biorąc pod uwagę przyszłość projektu, przydatne może być formalne zdefiniowanie niezbędnego zachowania klas.

lwm
źródło
5

Aby odpowiedzieć na pytanie: jest w tym coś więcej.

Jednym ważnym aspektem interfejsu jest zamiar.

Interfejs to „typ abstrakcyjny, który nie zawiera danych, ale ujawnia zachowania” - Interfejs (obliczeniowy) Więc jeśli jest to zachowanie lub zestaw zachowań obsługiwanych przez klasę, to prawdopodobnie interfejs jest prawidłowym wzorcem. Jeśli jednak zachowanie (-a) jest (są) nieodłączne od koncepcji zawartej w klasie, prawdopodobnie prawdopodobnie nie potrzebujesz interfejsu.

Pierwszym pytaniem, jakie należy zadać, jest natura rzeczy lub procesu, który próbujesz reprezentować. Następnie zapoznaj się z praktycznymi przyczynami wdrożenia tej natury w określony sposób.

Joshua Drake
źródło
5

Skoro zadałeś to pytanie, przypuszczam, że już widziałeś, jak interfejs ukrywa wiele implementacji. Może to objawiać się zasadą inwersji zależności.

Jednak konieczność posiadania interfejsu nie zależy od liczby jego implementacji. Prawdziwa rola interfejsu polega na tym, że definiuje on umowę określającą, jaka usługa powinna być świadczona, a nie jak powinna być realizowana.

Po zdefiniowaniu umowy dwa lub więcej zespołów może pracować niezależnie. Załóżmy, że pracujesz nad modułem A i zależy to od modułu B, fakt utworzenia interfejsu na B pozwala kontynuować pracę bez obawy o implementację B, ponieważ wszystkie szczegóły są ukryte przez interfejs. W ten sposób programowanie rozproszone staje się możliwe.

Chociaż moduł B ma tylko jedną implementację swojego interfejsu, interfejs jest nadal niezbędny.

Podsumowując, interfejs ukrywa szczegóły implementacji przed użytkownikami. Programowanie interfejsu pomaga w pisaniu większej liczby dokumentów, ponieważ należy zdefiniować umowę, pisać bardziej modułowe oprogramowanie, promować testy jednostkowe i przyspieszyć rozwój.

Hui Wang
źródło
2
Każda klasa może mieć interfejs publiczny (metody publiczne) i interfejs prywatny (szczegóły implementacji). Mógłbym argumentować, że publicznym interfejsem klasy jest umowa. Nie potrzebujesz dodatkowego elementu do pracy przy tej umowie.
Fuhrmanator
4

Wszystkie odpowiedzi tutaj są bardzo dobre. Rzeczywiście przez większość czasu nie trzeba wdrażać innego interfejsu. Ale są przypadki, w których i tak możesz chcieć to zrobić. Oto kilka przypadków, w których to robię:

Klasa implementuje inny interfejs, którego nie chcę
często zdarzać, za pomocą klasy adaptera, która mostkuje kod innej firmy.

interface NameChangeListener { // Implemented by a lot of people
    void nameChanged(String name); 
} 

interface NameChangeCount { // Only implemented by my class
    int getCount();
}

class NameChangeCounter implements NameChangeListener, NameChangeCount {
    ...
}

class SomeUserInterface {
    private NameChangeCount currentCount; // Will never know that you can change the counter
}

Klasa korzysta ze specyficznej technologii, która nie powinna przeciekać.
Głównie podczas interakcji z bibliotekami zewnętrznymi. Nawet jeśli jest tylko jedna implementacja, używam interfejsu, aby upewnić się, że nie wprowadzę niepotrzebnego sprzężenia z biblioteką zewnętrzną.

interface SomeRepository { // Guarantee that the external library details won't leak trough
    ...
}

class OracleSomeRepository implements SomeRepository { 
    ... // Oracle prefix allow us to quickly know what is going on in this class
}

Komunikacja między warstwami
Nawet jeśli tylko jedna klasa interfejsu użytkownika kiedykolwiek implementuje jedną z klas domeny, pozwala to na lepszą separację między tymi warstwami, a co najważniejsze, pozwala uniknąć cyklicznej zależności.

package project.domain;

interface UserRequestSource {
    public UserRequest getLastRequest();
}

class UserBehaviorAnalyser {
    private UserRequestSource requestSource;
}

package project.ui;

class OrderCompleteDialog extends SomeUIClass implements project.domain.UserRequestSource {
    // UI concern, no need for my domain object to know about this method.
    public void displayLabelInErrorMode(); 

    // They most certainly need to know about *that* though
    public UserRequest getLastRequest();
}

Dla większości obiektów powinien być dostępny tylko podzbiór metody.
Najczęściej dzieje się tak, gdy mam jakąś metodę konfiguracji konkretnej klasy

interface Sender {
    void sendMessage(Message message)
}

class PacketSender implements Sender {
    void sendMessage(Message message);
    void setPacketSize(int sizeInByte);
}

class Throttler { // This class need to have full access to the object
    private PacketSender sender;

    public useLowNetworkUsageMode() {
        sender.setPacketSize(LOW_PACKET_SIZE);
        sender.sendMessage(new NotifyLowNetworkUsageMessage());

        ... // Other details
    }
}

class MailOrder { // Not this one though
    private Sender sender;
}

W końcu używam interfejsu z tego samego powodu, dla którego używam pola prywatnego: inny obiekt nie powinien mieć dostępu do rzeczy, do których nie powinien mieć dostępu. Jeśli mam taki przypadek, wprowadzam interfejs, nawet jeśli implementuje go tylko jedna klasa.

Laurent Bourgault-Roy
źródło
2

Interfejsy są naprawdę ważne, ale staraj się kontrolować ich liczbę.

Po stworzeniu interfejsów do prawie wszystkiego łatwo jest skończyć z kodem „posiekanego spaghetti”. Opieram się na większej mądrości Ayende Rahien, która opublikowała kilka bardzo mądrych słów na ten temat:

http://ayende.com/blog/153889/limit-your-abstractions-analyzing-a-ddd-application

To jego pierwszy post z całej serii, więc czytaj dalej!

Lord Spa
źródło
kod „posiekanego spaghetti” jest również znany jako kod ravioli c2.com/cgi/wiki?RavioliCode
CurtainDog
Dla mnie brzmi bardziej jak kod lasagne lub kod baklava - kod ze zbyt wieloma warstwami. ;-)
dodgy_coder
2

Jednym z powodów, dla których nadal możesz chcieć wprowadzić interfejs w tym przypadku, jest przestrzeganie zasady inwersji zależności . Oznacza to, że moduł korzystający z tej klasy będzie zależał od jej abstrakcji (tj. Interfejsu) zamiast od konkretnej implementacji. Oddziela komponenty wysokiego poziomu od komponentów niskiego poziomu.

dodgy_coder
źródło
2

Nie ma prawdziwego powodu, aby cokolwiek robić. Interfejsy mają ci pomóc, a nie program wyjściowy. Więc nawet jeśli interfejs jest implementowany przez milion klas, nie ma reguły, która mówi, że musisz go utworzyć. Tworzysz taki, aby gdy Ty lub ktokolwiek inny, kto używa Twojego kodu, chciałbyś coś zmienić, przenosi się on na wszystkie implementacje. Stworzenie interfejsu pomoże ci we wszystkich przyszłych przypadkach, w których możesz chcieć stworzyć inną klasę, która go implementuje.

John Demetriou
źródło
1

Nie zawsze trzeba definiować interfejs dla klasy.

Proste obiekty, takie jak obiekty wartości, nie mają wielu implementacji. Nie trzeba ich też wyśmiewać. Implementacja może być testowana samodzielnie, a gdy testowane są inne klasy, które od nich zależą, można użyć obiektu wartości rzeczywistej.

Pamiętaj, że utworzenie interfejsu ma swój koszt. Musi być aktualizowany wzdłuż implementacji, potrzebuje dodatkowego pliku, a niektóre IDE będą miały problemy z powiększaniem implementacji, a nie interfejsu.

Zdefiniowałbym więc interfejsy tylko dla klas wyższego poziomu, gdzie chcesz abstrakcji od implementacji.

Zauważ, że dzięki klasie otrzymasz interfejs za darmo. Oprócz implementacji klasa definiuje interfejs z zestawu metod publicznych. Interfejs ten jest implementowany przez wszystkie klasy pochodne. Nie jest to ściśle interfejs, ale można go używać dokładnie w ten sam sposób. Nie sądzę więc, aby konieczne było odtworzenie interfejsu, który już istnieje pod nazwą klasy.

Florian F.
źródło