Czy nie chodzi o to, że wiele klas przestrzega zestawu reguł i implementacji?
Czy nie chodzi o to, że wiele klas przestrzega zestawu reguł i implementacji?
Odpowiedzi:
Ś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.
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:
Foo
odnosić się bezpośrednio do klasy BarImpl
, zdecydowanie zobowiązujesz się do zmiany za Foo
każdym razem, gdy ją zmieniasz BarImpl
. Zasadniczo traktujesz je jako jedną jednostkę kodu podzieloną na dwie klasy.Foo
odwołasz się do interfejsu Bar
, zobowiązujesz się do tego, aby uniknąć zmian w Foo
momencie 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 Foo
celu użycia interfejsu Bar
, zwykle można dokonać ponownej kompilacji BarImpl
bez konieczności ponownej kompilacji Foo
. W dużych aplikacjach może to zaoszczędzić dużo czasu.
Foo
zależy od interfejsu Bar
następnie BarImpl
mogą 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).
If you make class Foo refer directly to class BarImpl, you're strongly committing yourself to change Foo every time you change BarImpl
Po 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. BarImpl
Implementuje Bar. W scenariuszu z wieloma klasami rozumiem zasadę odwrócenia zależności i to, jak interfejs jest pomocny.
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)
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.
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 = mockImplementation
w 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ż:
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.
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 Car
klasę. W swojej klasie na pewno potrzebujesz warstwy interfejsu użytkownika. W tym konkretnym przykładzie, przybiera formę IginitionSwitch
, SteeringWheel
, GearShift
, GasPedal
i 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:
Właściwości te tworzą domenę z poniższych IgnitionSwitch
, czyli innymi słowy, co wie na temat i jest odpowiedzialny za.
IgnitionSwitch
Nie 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, IgnitionSwitch
któ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.
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.
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.
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.
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.
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!
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.
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.
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.