Czy nie chodzi o to, że wiele klas przestrzega zestawu reguł i implementacji?
design-patterns
coding-standards
interfaces
Lamin Sanneh
źródło
źródło
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.
źródło
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 klasyBarImpl
, zdecydowanie zobowiązujesz się do zmiany zaFoo
każdym razem, gdy ją zmieniaszBarImpl
. Zasadniczo traktujesz je jako jedną jednostkę kodu podzieloną na dwie klasy.Foo
odwołasz się do interfejsuBar
, zobowiązujesz się do tego, aby uniknąć zmian wFoo
momencie zmianyBarImpl
.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 interfejsuBar
, zwykle można dokonać ponownej kompilacjiBarImpl
bez konieczności ponownej kompilacjiFoo
. W dużych aplikacjach może to zaoszczędzić dużo czasu.źródło
Foo
zależy od interfejsuBar
następnieBarImpl
mogą być modyfikowane bez konieczności rekompilacjiFoo
. 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)
źródło
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.
źródło
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ż:
źródło
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:
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
iBrakePedal
. Ponieważ ten samochód zawieraAutomaticTransmission
, nie potrzebujeszClutchPedal
. (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:
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:
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 zaGasPedal
. 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.źródło
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.
źródło
Z MSDN :
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.
źródło
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.
źródło
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.
źródło
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.
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ą.
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.
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
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.
źródło
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!
źródło
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.
źródło
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.
źródło
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.
źródło