Czy to zły nawyk nieużywania interfejsów? [Zamknięte]

40

Używam interfejsów rzadko i uważam je za wspólne w innych kodach.

Poza tym w kodzie rzadko tworzę podklasy i superklasy (tworząc własne klasy).

  • Czy to źle?
  • Czy sugerowałbyś zmianę tego stylu?
  • Czy ten styl ma jakieś skutki uboczne?
  • Czy to dlatego, że nie pracowałem przy dużych projektach?
jineesh joseph
źródło
10
Jaki to język?
2
Poza jakim językiem, jaki to paradygmat?
Armando
1
Czy koncepcja „interfejsu” nie wykracza poza język? Java ma wbudowany typ interfejsu, ale programiści C ++ często również tworzą interfejsy (poprzedzając je I) i wszystkie służą temu samemu celowi.
Ricket
4
@Ricket: Nie, nie bardzo. Problem polega na tym, że C ++ oferuje dziedziczenie wielu klas. W C ++ „interfejs” jest o wiele bardziej konceptualny, a w rzeczywistości używamy tego, co określiliby jako abstrakcyjną klasę podstawową. #
DeadMG
2
@Ricket nie, szczególnie jeśli pracujesz w języku funkcjonalnym, a nie proceduralnym lub OOP.
alternatywny

Odpowiedzi:

36

Istnieje kilka powodów, dla których warto używać interfejsów:

  1. Interfejsy są odpowiednie w sytuacjach, w których aplikacje wymagają wielu potencjalnie niepowiązanych typów obiektów, aby zapewnić określoną funkcjonalność.
  2. Interfejsy są bardziej elastyczne niż klasy podstawowe, ponieważ można zdefiniować pojedynczą implementację, która może implementować wiele interfejsów.
  3. Interfejsy są lepsze w sytuacjach, w których nie trzeba dziedziczyć implementacji z klasy podstawowej.
  4. 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.

http://msdn.microsoft.com/en-us/library/3b5b8ezk(v=vs.80).aspx

Interfejsy są jak wszystko inne w programowaniu. Jeśli ich nie potrzebujesz, nie używaj ich. Widziałem je szeroko stosowane ze względu na styl, ale jeśli nie potrzebujesz specjalnych właściwości i możliwości, które zapewnia interfejs, nie widzę korzyści z używania ich „tylko dlatego, że”.

Robert Harvey
źródło
13
Jego język nie jest określony, a twój jest zbyt specyficzny - na przykład w C ++ struktura może rzeczywiście dziedziczyć po klasie i można pomnożyć dziedziczenie nieabstrakcyjnych baz.
DeadMG
2
Ta odpowiedź wydaje się być C #, ale dotyczy również Javy, jeśli wyjmiesz numer 4, i tylko w pewnym stopniu dotyczy C ++, ponieważ w C ++ dozwolone jest wielokrotne dziedziczenie.
Ricket
2
Nie zgadzam się również z ostatnim stwierdzeniem. Myślę, że istnieje wiele dobrych praktyk, które programiści powinni stosować, ale nie robią tego, ponieważ uważają, że ich nie potrzebują. Myślę, że pytający jest bardzo mądry, rozglądając się wokół, widząc, że wszyscy to robią, i myślę, że może on też powinien to robić, ale najpierw pytając „dlaczego”.
Ricket
3
@Ricket: Dlaczego w czterech punktach mojej odpowiedzi. Jeśli żaden z tych powodów nie ma zastosowania, nie potrzebujesz interfejsu.
Robert Harvey,
17

Zarówno dziedziczenie klas, jak i interfejsy mają swoje miejsce. Dziedziczenie oznacza „jest”, podczas gdy interfejs zawiera umowę określającą, jak coś „zachowuje się”.

Powiedziałbym, że częstsze używanie interfejsów wcale nie jest złą praktyką. Obecnie czytam „Skuteczne C # - 50 konkretnych sposobów na poprawę twojego C #” Billa Wagnera. W pozycji nr 22 znajduje się cytat „Wolę definiowanie i wdrażanie interfejsów niż dziedziczenie”.

Zasadniczo używam klas podstawowych, gdy muszę zdefiniować konkretną implementację typowego zachowania między typami pokrewnymi. Częściej korzystam z interfejsów. W rzeczywistości zwykle zaczynam od zdefiniowania interfejsu dla klasy, kiedy zaczynam ją tworzyć ... nawet jeśli ostatecznie nie skompiluję interfejsu, stwierdzę, że pomaga to od zdefiniowania publicznego interfejsu API klasa od samego początku. Jeśli stwierdzę, że mam wiele klas, które zarówno implementują interfejs, a logika implementacji jest identyczna, dopiero wtedy zadam sobie pytanie, czy sensowne byłoby wdrożenie wspólnej klasy podstawowej między typami.

Kilka cytatów z książki Billa Wagnersa ...

wszystkie klasy pochodne natychmiast uwzględniają to zachowanie. Dodanie elementu do interfejsu powoduje uszkodzenie wszystkich klas, które implementują ten interfejs. Nie będą zawierać nowej metody i nie będą się już kompilować. Każdy implementator musi zaktualizować ten typ, aby uwzględnić nowego członka. Wybór między abstrakcyjną klasą bazową a interfejsem to pytanie, jak najlepiej wspierać swoje abstrakcje w czasie. Interfejsy są naprawione: interfejs wydajesz jako umowę dotyczącą zestawu funkcji, które może zaimplementować każdy typ. Klasy podstawowe mogą być z czasem przedłużane. Rozszerzenia te stają się częścią każdej klasy pochodnej. Oba modele mogą być mieszane w celu ponownego wykorzystania kodu implementacji przy jednoczesnym wsparciu wielu interfejsów. ” Nie będą zawierać nowej metody i nie będą się już kompilować. Każdy implementator musi zaktualizować ten typ, aby uwzględnić nowego członka. Wybór między abstrakcyjną klasą bazową a interfejsem to pytanie, jak najlepiej wspierać swoje abstrakcje w czasie. Interfejsy są naprawione: interfejs wydajesz jako umowę dotyczącą zestawu funkcji, które może zaimplementować każdy typ. Klasy podstawowe mogą być z czasem przedłużane. Rozszerzenia te stają się częścią każdej klasy pochodnej. Oba modele mogą być mieszane w celu ponownego wykorzystania kodu implementacji przy jednoczesnym wsparciu wielu interfejsów. ” Nie będą zawierać nowej metody i nie będą się już kompilować. Każdy implementator musi zaktualizować ten typ, aby uwzględnić nowego członka. Wybór między abstrakcyjną klasą bazową a interfejsem to pytanie, jak najlepiej wspierać swoje abstrakcje w czasie. Interfejsy są naprawione: interfejs wydajesz jako umowę dotyczącą zestawu funkcji, które może zaimplementować każdy typ. Klasy podstawowe mogą być z czasem przedłużane. Rozszerzenia te stają się częścią każdej klasy pochodnej. Oba modele mogą być mieszane w celu ponownego wykorzystania kodu implementacji przy jednoczesnym wsparciu wielu interfejsów. ” Wydajesz interfejs jako umowę dotyczącą zestawu funkcji, które może zaimplementować każdy typ. Klasy podstawowe mogą być z czasem przedłużane. Rozszerzenia te stają się częścią każdej klasy pochodnej. Oba modele mogą być mieszane w celu ponownego wykorzystania kodu implementacji przy jednoczesnym wsparciu wielu interfejsów. ” Wydajesz interfejs jako umowę dotyczącą zestawu funkcji, które może zaimplementować każdy typ. Klasy podstawowe mogą być z czasem przedłużane. Rozszerzenia te stają się częścią każdej klasy pochodnej. Oba modele mogą być mieszane w celu ponownego wykorzystania kodu implementacji przy jednoczesnym wsparciu wielu interfejsów. ”

„Interfejsy kodowania zapewniają większą elastyczność innym programistom niż kodowanie do typów klas podstawowych”.

„Używanie interfejsów do definiowania interfejsów API dla klasy zapewnia większą elastyczność”.

„Gdy Twój typ udostępnia właściwości jako typy klas, udostępnia cały interfejs dla tej klasy. Korzystając z interfejsów, możesz ujawnić tylko metody i właściwości, których mają używać klienci.”

„Klasy podstawowe opisują i wdrażają typowe zachowania w pokrewnych konkretnych typach. Interfejsy opisują atomowe elementy funkcjonalności, które mogą być implementowane przez niepowiązane konkretne typy. Obie mają swoje miejsce. Klasy definiują tworzone typy. Interfejsy opisują zachowanie tych typów jako elementy funkcjonalności. Jeśli zrozumiesz różnice, stworzysz bardziej wyraziste projekty, które będą bardziej odporne na zmiany. Użyj hierarchii klas, aby zdefiniować typy pokrewne. Ujawnij funkcjonalność za pomocą interfejsów zaimplementowanych we wszystkich tych typach. ”

John Connelly
źródło
2
Cóż, przeczytałem to i pomyślałem, że to dobra odpowiedź.
Eric King,
1
@ mc10 Nie jestem pewien, na czym polega problem. Odniósł się do książki, a dolna połowa jego odpowiedzi to tylko cytaty z tej książki, którą wyraźnie informuje, na wypadek gdybyś nie chciał tracić czasu.
Pete,
@Pete Nie powiedziałem, że odpowiedź jest zła, ale tylko, że była trochę za długa. Wątpię, że czasami potrzebowałeś całego cytatu.
kevinji
3
haha, jeśli to jest tl; dr, nie jestem pewien, czy spodoba ci się ta cała StackExchange-wysokiej jakości odpowiedzi.
Nic
haha, dzięki chłopaki. Tak, przyszło mi do głowy, że odpowiedź była nieco długa, ale pomyślałem, że kwalifikuję swoją odpowiedź, dołączając odpowiednie cytaty pana Wagnera, które wyjaśniają zalety i wady interfejsów w porównaniu do dziedziczenia, a także scenariusze, w których każdy jest bardziej odpowiedni. Być może bezpośrednie cytowanie książki nie dodało dużej wartości mojej odpowiedzi.
John Connelly,
8

Jedną z rzeczy, o których nie wspomniano, jest testowanie: we wszystkich bibliotekach próbnych języka C #, a także w Javie, klas nie można wyśmiewać, chyba że implementują interfejs. Prowadzi to wiele projektów zgodnie z praktykami Agile / TDD, aby nadać każdej klasie własny interfejs.

Niektórzy ludzie uważają tę najlepszą praktykę, ponieważ „zmniejsza ona sprzężenie”, ale nie zgadzam się - myślę, że jest to tylko obejście niedoboru języka.


Myślę, że interfejsów najlepiej używać, gdy masz dwie lub więcej klas, które abstrakcyjnie wykonują „to samo”, ale na różne sposoby.

Na przykład .NET Framework ma wiele klas, które przechowują listy rzeczy , ale wszystkie przechowują te rzeczy na różne sposoby. Dlatego sensowne jest posiadanie abstrakcyjnego IList<T>interfejsu, który można wdrożyć za pomocą różnych metod.

Powinieneś go również użyć, jeśli chcesz, aby klasy 2+ były wymienne lub wymienne w przyszłości. Jeśli w przyszłości pojawi się nowy sposób przechowywania list AwesomeList<T>, to zakładając, że używasz IList<T>całego kodu, zmiana go na użycie AwesomeList<T>oznaczałaby zmianę kilkudziesięciu wierszy, a nie kilkuset / tysięcy.

BlueRaja - Danny Pflughoeft
źródło
5

Głównym skutkiem niestosowania dziedziczenia i odpowiednich interfejsów jest ścisłe połączenie . To może być trochę trudne do nauczenia się rozpoznawać, ale zwykle najbardziej oczywistym objawem jest to, że po dokonaniu zmiany często musisz przejrzeć całą masę innych plików, aby wprowadzić zmiany w wyniku tętnienia.

Karl Bielefeldt
źródło
4

Nie, wcale nie jest źle. Należy stosować jawne interfejsy, wtedy i tylko wtedy, gdy dwie klasy ze wspólnym interfejsem muszą być wymienne w czasie wykonywania. Jeśli nie muszą być wymienne, nie dziedziczą. To takie proste. Dziedziczenie jest kruche i powinno się go unikać w miarę możliwości. Jeśli możesz tego uniknąć lub zamiast tego użyć leków ogólnych, zrób to.

Problem polega na tym, że w językach takich jak C # i Java ze słabymi generycznymi czasami kompilacji możesz w końcu naruszyć DRY, ponieważ nie ma sposobu na napisanie jednej metody, która poradziłaby sobie z więcej niż jedną klasą, chyba że wszystkie klasy odziedziczą z tej samej bazy. C # 4 dynamic mogą sobie z tym poradzić.

Rzecz w tym, że dziedziczenie jest jak zmienne globalne - kiedy go dodasz, a twój kod będzie od niego zależał, Boże, pomóż mu go zabrać. Można go jednak dodać w dowolnym momencie, a nawet dodać bez zmiany klasy podstawowej za pomocą pewnego rodzaju opakowania.

DeadMG
źródło
1
Powód odmowy głosowania?
DeadMG
Nie jestem pewien, proszę wyrazić opinię. To była dobra odpowiedź.
Bryan Boettcher
3

Tak, nie (lub zbyt rzadko) używanie interfejsów jest prawdopodobnie złą rzeczą. Interfejsy (w sensie abstrakcyjnym, a konstrukcja języka C # / Java jest całkiem dobrym przybliżeniem tego abstrakcyjnego sensu) definiują wyraźne punkty interakcji między systemami i podsystemami. Pomaga to zmniejszyć sprzężenie i sprawia, że ​​system jest łatwiejszy w utrzymaniu. Jak wszystko, co poprawia łatwość konserwacji, staje się tym ważniejsze, im większy jest system.

Michael Borgwardt
źródło
3

Nie używałem interfejsu od lat. Oczywiście dzieje się tak, ponieważ programuję prawie wyłącznie w Erlangu od lat i cała koncepcja interfejsu po prostu nie istnieje. (Najbliższe jest „zachowanie” i tak naprawdę nie jest to to samo, chyba że zmrużysz oczy naprawdę i nie spojrzysz na nie kątem oka.)

Tak naprawdę twoje pytanie jest zależne od paradygmatu (w tym przypadku OOP), a ponadto jest bardzo zależne od języka (istnieją języki OOP bez interfejsów).

WŁAŚNIE MOJA poprawna OPINIA
źródło
2

Jeśli mówisz o korzystaniu z Javy, jednym z powodów korzystania z interfejsów jest to, że umożliwiają one używanie obiektów proxy bez bibliotek do generowania kodu. Może to być znaczną zaletą, gdy pracujesz ze złożonymi ramami, takimi jak Spring. Co więcej, niektóre funkcje wymagają interfejsów: RMI jest klasycznym przykładem tego, ponieważ musisz opisać funkcjonalność, którą udostępniasz, w kategoriach interfejsów (które dziedziczą java.rmi.Remote), niezależnie od tego, czy je wdrażasz.

Donal Fellows
źródło
1

Rzeczy się zmieniają ( MS Moles ), ale głównym powodem, dla którego uważam, że kodowanie prawie wyłącznie do interfejsów jest dobre, jest to, że łatwo się z nich kpić i naturalnie pasują do architektury IoC.

IMO powinieneś zawsze pracować tylko z interfejsami lub całkowicie głupimi DAO tam, gdzie to możliwe. Gdy przejdziesz do tego sposobu myślenia i zaczniesz korzystać z biblioteki, która nie ujawnia się przez interfejsy i robi wszystko za pomocą konkretnych obiektów, muszę być szczery, wszystko wydaje się trochę niezgrabne.


źródło
0

W przypadku starych projektów modowych używa się interfejsów, aby uniknąć odwołań cyklicznych, ponieważ odniesienia długoterminowe mogą stać się poważnym problemem w zakresie konserwacji na dłuższą metę.

Zły:

class B; // forward declaration
class A
{
  B* b;
};

class B
{
  A* a;
}

Nie jest zły:

class PartOfClassB_AccessedByA
{
};

class A
{
  PartOfClassB_AccessedByA* b;
};

class B : public PartOfClassB_AccessedByA
{
  A* a;
}

Zwykle A, B, PartOfClassB_AccessedByA zaimplementowane z osobnymi plikami.

9dan
źródło
0

Programowanie oparte na interfejsie pomaga uczynić kod bardziej elastycznym i łatwiejszym do testowania. Klasy implementacji można zmienić bez dotykania kodu klienta [Elastyczność]. Podczas testowania kodu można zastąpić faktyczne programowanie oparte na interfejsie, co czyni kod bardziej elastycznym i łatwiejszym do testowania. Klasy implementacji można zmienić bez dotykania kodu klienta [Elastyczność]. Podczas testowania kodu można zastąpić rzeczywisty obiekt próbnymi obiektami [Testability] .bject obiektami próbnymi [Testability].

Amit Patel
źródło