Interfejs a klasa podstawowa


767

Kiedy powinienem używać interfejsu, a kiedy powinienem używać klasy bazowej?

Czy powinien to zawsze być interfejs, jeśli nie chcę definiować podstawowej implementacji metod?

Jeśli mam klasę psów i kotów. Dlaczego miałbym chcieć wdrożyć IPet zamiast PetBase? Rozumiem, że mam interfejsy dla ISheds lub IBarków (IMakesNoise?), Ponieważ mogą one być umieszczane na zwierzęciu za zwierzę, ale nie rozumiem, którego użyć dla ogólnego zwierzaka.


11
Uważam tylko, że powinieneś wziąć pod uwagę - interfejsy mogą mieć wiele ograniczeń, o których możesz nie wiedzieć, aż do bardzo późnych etapów. Na przykład w .NET nie można serializować zmiennej członka interfejsu, więc jeśli masz klasowe zoo i tablicę zmiennych członków IAnimals, nie będziesz mógł serializować zoo (a to oznacza, że ​​pisanie WebServices lub innych rzeczy wymagających serializacji byłoby ból).
synhershko

1
To pytanie może pomóc w zrozumieniu koncepcji interfejsów. stackoverflow.com/q/8531292/1055241
gprathour

Jestem po prostu ciekaw. Poznałem w CLR za pośrednictwem C # poniższym fragmencie: I tend to prefer using the interface technique over the base type technique because the base type technique doesn’t allow the developer to choose the base type that works best in a particular situation.. Nie mogę zrozumieć, co oznacza fragment. Możemy utworzyć kilka typów podstawowych i utworzyć typ pochodny dla dowolnego z nich, aby programista mógł wybrać typ podstawowy. Czy ktoś mógłby wyjaśnić, czego mi brakuje? Wierzę, że może to być część tego pytania. A może powinienem opublikować kolejny na temat konkretnego fragmentu?
qqqqqqq

Odpowiedzi:


501

Weźmy przykład klasy Dog and Cat i zilustrujmy za pomocą C #:

Zarówno pies, jak i kot są zwierzętami, w szczególności czworonożnymi ssakami (zwierzęta są zbyt ogólne). Załóżmy, że masz abstrakcyjną klasę Mammal dla obu z nich:

public abstract class Mammal

Ta klasa bazowa prawdopodobnie będzie miała domyślne metody, takie jak:

  • Karmić
  • Kumpel

Wszystkie są zachowaniami, które mają mniej więcej taką samą implementację między oboma gatunkami. Aby to zdefiniować, będziesz mieć:

public class Dog : Mammal
public class Cat : Mammal

Załóżmy teraz, że istnieją inne ssaki, które zwykle zobaczymy w zoo:

public class Giraffe : Mammal
public class Rhinoceros : Mammal
public class Hippopotamus : Mammal

Będzie to nadal ważne, ponieważ u podstaw funkcjonalności Feed()i Mate()pozostanie takie samo.

Jednak żyrafy, nosorożce i hipopotamy nie są dokładnie zwierzętami, z których można zrobić zwierzęta domowe. W tym miejscu przydatny będzie interfejs:

public interface IPettable
{
    IList<Trick> Tricks{get; set;}
    void Bathe();
    void Train(Trick t);
}

Realizacja powyższej umowy nie będzie taka sama między kotem i psem; umieszczenie ich implementacji w klasie abstrakcyjnej do dziedziczenia będzie złym pomysłem.

Twoje definicje psów i kotów powinny teraz wyglądać następująco:

public class Dog : Mammal, IPettable
public class Cat : Mammal, IPettable

Teoretycznie możesz zastąpić je z wyższej klasy podstawowej, ale w istocie interfejs pozwala dodawać do klasy tylko to, czego potrzebujesz, bez potrzeby dziedziczenia.

W związku z tym, ponieważ zwykle możesz dziedziczyć tylko jedną klasę abstrakcyjną (w większości statycznie typowanych języków OO, czyli ... wyjątki obejmują C ++), ale możesz implementować wiele interfejsów, pozwala to na konstruowanie obiektów ściśle według wymagań .


149
Nie sądzę, że to takie proste. Lekko zmieniłeś pytanie (wymagania), aby interfejs miał więcej sensu. Zawsze powinieneś zadać sobie pytanie, czy definiujesz umowę (interfejs) czy wspólną implementację (klasa bazowa).
David Pokluda

18
Interfejs jest umową. Ujawniasz tylko część umowy, której wymaga usługa. Jeśli masz „PettingZoo”, na pewno nie chcesz ujawniać „wiązania” użytkownikowi!
Anthony Mastrean,

10
@David Touche, chociaż zrobiłem to, aby lepiej zilustrować, do czego służy interfejs i czym jest abstrakcyjna klasa dla jego zrozumienia. Pies i kot nie wydają się sztywnym wymogiem!
Jon Limjap

2
Należy zauważyć, że w interpretowanych środowiskach JIT (szczególnie JVM) wirtualne wywołania metod są znacznie szybsze niż wywołania metod interfejsu , w zależności od kontekstu. Podkreślam „kontekst”, ponieważ JVM często może zoptymalizować wolne wyszukiwanie metod. (np. jeśli lista dziedziczących interfejsów to zwykle instancje tej samej klasy. To niestety utrudnia przeprowadzenie testów porównawczych). Jeśli próbujesz zoptymalizować coś, co jest wrażliwe na wydajność, i tylko wtedy, powinieneś to rozważyć.
Philip Guin

14
Interfejs pozwala również na skałę dla zwierząt domowych, pozwalając jej kąpać się i uczyć sztuczek, ale karmienie i krycie nie byłoby obsługiwane, ponieważ byłoby to śmieszne dla skały.
eclux,

146

Cóż, Josh Bloch powiedział w Effective Java 2d :

Wolę interfejsy niż klasy abstrakcyjne

Niektóre główne punkty:

  • Istniejące klasy można łatwo zmodyfikować w celu wdrożenia nowego interfejsu . Wszystko, co musisz zrobić, to dodać wymagane metody, jeśli jeszcze nie istnieją, i dodać klauzulę implements do deklaracji klasy.

  • Interfejsy są idealne do definiowania mixin . Luźno mówiąc, mixin jest typem, który klasa może zaimplementować oprócz swojego „podstawowego typu”, aby zadeklarować, że zapewnia pewne opcjonalne zachowanie. Na przykład Porównywalny to interfejs mixin, który pozwala klasie zadeklarować, że jej instancje są uporządkowane względem innych wzajemnie porównywalnych obiektów.

  • Interfejsy umożliwiają konstruowanie struktur typu niehierarchicznego . Hierarchie typów świetnie nadają się do organizowania niektórych rzeczy, ale inne nie mieszczą się w sztywnej hierarchii.

  • Interfejsy umożliwiają bezpieczne, potężne ulepszenia funkcjonalności za pomocą idiomu klasy owijającej. Jeśli używasz klas abstrakcyjnych do definiowania typów, pozostawiasz programistę, który chce dodać funkcjonalność bez alternatywy, ale użyć dziedziczenia.

Ponadto można łączyć zalety interfejsów i klas abstrakcyjnych, zapewniając abstrakcyjną szkieletową klasę implementacji pasującą do każdego nietrywialnego interfejsu, który eksportujesz.

Z drugiej strony interfejsy są bardzo trudne do ewolucji. Dodanie metody do interfejsu spowoduje uszkodzenie wszystkich jej implementacji.

PS .: Kup książkę. Jest o wiele bardziej szczegółowy.


71
Ilekroć trzeba wprowadzić zmiany w interfejsie, niezłomnym sposobem na to jest stworzenie nowego interfejsu, który odziedziczy po starym. To zachowuje istniejące implementacje i pozwala ci robić, co chcesz w nowym.
Scott Lawrence,

5
mamy „zasadę segregacji interfejsów”. Ta zasada uczy, jak dbać o to, jak piszemy nasze interfejsy. Kiedy piszemy nasze interfejsy, powinniśmy zadbać o to, aby dodawać tylko metody, które powinny tam być. Jeśli dodamy metody, których nie powinno być, klasy implementujące interfejs również będą musiały zaimplementować te metody. Na przykład, jeśli utworzymy interfejs o nazwie Worker i dodamy metodę przerwy na lunch, wszyscy pracownicy będą musieli ją wdrożyć. Co jeśli pracownik jest robotem? Podsumowując Interfejsy zawierające metody, które nie są dla niego specyficzne, nazywane są interfejsami zanieczyszczonymi lub tłuszczowymi.
Eklavyaa

Od wersji Java 8 metody domyślne umożliwiają dodawanie nowych funkcji do interfejsów i zapewniają zgodność wsteczną dla istniejących klas, które implementują ten interfejs. Domyślne metody są wywoływane domyślnie, jeśli nie zostaną zastąpione w klasie implementującej. Wszystkie klasy implementujące mogą albo przesłonić domyślne metody, albo bezpośrednio je wywołać za pomocą instance.defaultMethod ()
Arpit Rastogi

118

Interfejsy i klasy podstawowe reprezentują dwie różne formy relacji.

Dziedziczenie (klasy podstawowe) reprezentuje relację „jest-a”. Na przykład pies lub kot „is-a” pet. Relacja ta zawsze reprezentuje (pojedynczy) cel klasy (w połączeniu z „zasadą jednej odpowiedzialności” ).

Z drugiej strony interfejsy reprezentują dodatkowe cechy klasy. Nazwałbym to relacją „jest”, jak w „ Foojest jednorazowy”, stąd IDisposableinterfejs w języku C #.


10
Spośród wszystkich odpowiedzi, ta daje najlepszą mieszankę zwięzłości bez utraty jasności
Matt Wilko

Ktoś kiedyś powiedział mi, żebym używał interfejsu, gdy istnieje relacja „nie ma”. Nie jestem jednak pewien, czy to zawsze prawda; Mój laptop ma ekran, więc czy laptop powinien implementować IScreen, czy mieć właściwość Screen? To ostatnie wydaje mi się bardziej naturalne.
Berend

1
@berend śmieszne, że piszesz to, ponieważ ekrany są realizowane za pośrednictwem interfejsów - VGA, HDMI itp.
Konstantin

Tylko dlatego, że to OOP, musimy rozwinąć naszą wyobraźnię, aby podążać za rzeczywistymi scenariuszami. Nie zawsze ma to zastosowanie.
Crismogram

110

Nowoczesny styl polega na definiowaniu IPet i PetBase.

Zaletą interfejsu jest to, że inny kod może go używać bez żadnych powiązań z innym kodem wykonywalnym. Całkowicie „czysty”. Interfejsy można również mieszać.

Ale klasy podstawowe są przydatne do prostych implementacji i wspólnych narzędzi. Zapewnij również abstrakcyjną klasę podstawową, aby zaoszczędzić czas i kod.


jedz swoje ciasto i jedz je też!
Darren Kopp,

3
Interfejs określa, w jaki sposób inne klasy mogą używać twojego kodu. Klasa podstawowa pomaga implementatorom w implementacji twojego interfejsu. Dwie różne rzeczy do dwóch różnych celów.
Bill Michell,

27
Nie ma tu nic „nowoczesnego”. Klasa podstawowa i interfejs z tym samym interfejsem API są po prostu nadmiarowe. W niektórych przypadkach możesz zastosować to podejście, ale nie powinieneś generalizować!
smentek

3
Najbardziej wspierany komentarz do drugiej obsługiwanej odpowiedzi faktycznie nie zgadza się z samą odpowiedzią, co jest interesujące. Muszę powiedzieć, że widzę wiele przykładów interfejsu i klasy bazowej. W tym sensie jest to „nowoczesny” sposób. Na przykład we wzorze MVVM faktycznie istnieje klasa ViewModelBase, która implementuje INotifyPropertyChanged. Ale kiedy mój kolega zapytał mnie, dlaczego mam klasę podstawową, zamiast implementować interfejs w każdym modelu widoku, nie wiem, jak go przekonać
tete

1
Poprawny. To nie jest kwestia jednego czy drugiego. Istnieją po to, aby rozwiązać 2 bardzo różne problemy. Interfejsy to umowy, które klasa wykonawcza musi rozwiązać. Znaleźli ostatnio przychylność (czasem fanatyczną) dla aspektów IoC i TDD. Klasy abstrakcyjne / podstawowe służą do grupowania hierarchicznie wspólnej logiki i właściwości. Redukuje zduplikowany kod, co z kolei zwiększa łatwość konserwacji rozwiązań i czyni go mniej podatnym na błędy.
ComeIn

63

Interfejsy

  • Definiuje umowę między 2 modułami. Nie może mieć żadnej implementacji.
  • Większość języków pozwala na implementację wielu interfejsów
  • Modyfikacja interfejsu jest przełomową zmianą. Wszystkie implementacje muszą zostać ponownie skompilowane / zmodyfikowane.
  • Wszyscy członkowie są publiczni. Wdrożenia muszą zaimplementować wszystkich członków.
  • Interfejsy pomagają w odsprzęganiu. Możesz użyć fałszywych frameworków, aby wykpić wszystko za interfejsem
  • Interfejsy zwykle wskazują na rodzaj zachowania
  • Implementacje interfejsu są od siebie oddzielone / odizolowane

Klasy podstawowe

  • Pozwala dodać domyślną implementację, którą otrzymujesz za darmo przez wyprowadzenie
  • Oprócz C ++ możesz wywodzić się tylko z jednej klasy. Nawet jeśli mogłoby to pochodzić z wielu klas, jest to zwykle zły pomysł.
  • Zmiana klasy podstawowej jest stosunkowo łatwa. Derywacje nie muszą robić nic specjalnego
  • Klasy podstawowe mogą deklarować funkcje chronione i publiczne, do których można uzyskać dostęp za pomocą pochodnych
  • Abstrakcyjne klasy podstawowe nie mogą być łatwo wyśmiewane jak interfejsy
  • Klasy podstawowe zwykle wskazują hierarchię typów (IS A)
  • Pochodne klas mogą zależeć od niektórych podstawowych zachowań (mają złożoną wiedzę na temat implementacji elementów nadrzędnych). Może być bałagan, jeśli zmienisz podstawową implementację dla jednego faceta i złamiesz pozostałych.

Uwaga: wytyczne projektowania ram zalecają używanie klas podstawowych (w przeciwieństwie do interfejsów), ponieważ są one lepsze. Dodanie nowej metody do abstrakcyjnej klasy bazowej w vNext jest niezłomną zmianą.
Gishu

59

Ogólnie rzecz biorąc, powinieneś faworyzować interfejsy zamiast klas abstrakcyjnych. Jednym z powodów użycia klasy abstrakcyjnej jest to, że masz wspólną implementację między konkretnymi klasami. Oczywiście nadal powinieneś zadeklarować interfejs (IPet) i mieć implementację tego interfejsu klasy abstrakcyjnej (PetBase). Korzystając z małych, wyraźnych interfejsów, możesz użyć wielokrotności, aby jeszcze bardziej zwiększyć elastyczność. Interfejsy pozwalają na maksymalną elastyczność i przenośność typów ponad granicami. Podczas przekazywania referencji przez granice zawsze należy przekazywać interfejs, a nie konkretny typ. Pozwala to odbiorcy określić konkretną implementację i zapewnia maksymalną elastyczność. Jest to absolutnie prawdziwe, gdy programujesz w sposób TDD / BDD.

Gang Czterech stwierdził w swojej książce „Ponieważ dziedziczenie wystawia podklasę na szczegóły dotyczące implementacji jej rodzica, często mówi się, że„ dziedziczenie przerywa enkapsulację ”. Wierzę, że to prawda.


Tak. Osobiście uważam, że to tyłek w tył. Interfejsy powinny posiadać absolutną minimalną funkcjonalność dla typu, a klasy podstawowe powinny zapewniać bogatą strukturę, na której można budować dostosowanie. Umieść to w interfejsie i bardzo utrudniłeś wdrożenie.

Nikt nie powiedział, że twoje interfejsy muszą być ogromne. Mniejsze, wiele interfejsów i bogatsze klasy bazowe stanowią świetne API.
Kilhoffer,

3
Czy to tylko ja, czy też większość klas „zwykłych pracowników” ma wspólną implementację? W tym kontekście jest to sprzeczne z ogólną zasadą faworyzowania interfejsów. Przekształciłbym twoje uogólnienie w 2 uogólnienia: te wspólne klasy, które nie zawierają żadnej logiki lub mają małą logikę, powinny implementować interfejs. Te wspólne klasy, które zawierają „przyzwoitą” logikę, powinny wywodzić się z klasy podstawowej (ponieważ najprawdopodobniej będą one miały wspólną funkcjonalność).
goku_da_master

@Kilhoffer „Interfejsy pozwalają na maksymalną elastyczność i przenośność typów ponad granicami”, opracuj to oświadczenie.
JAVA

49

Jest to dość specyficzne dla platformy .NET, ale książka Framework Design Guidelines twierdzi, że ogólnie klasy zapewniają większą elastyczność w ewoluujących ramach. Po dostarczeniu interfejsu nie ma możliwości jego zmiany bez zerwania kodu używającego tego interfejsu. Jednak dzięki klasie możesz ją modyfikować i nie łamać kodu, który do niej prowadzi. Tak długo, jak dokonasz odpowiednich modyfikacji, w tym dodając nowe funkcje, będziesz w stanie rozszerzać i rozwijać swój kod.

Krzysztof Cwalina mówi na stronie 81:

W trakcie trzech wersji .NET Framework rozmawiałem o tych wytycznych z kilkoma programistami w naszym zespole. Wielu z nich, w tym ci, którzy początkowo nie zgadzali się z wytycznymi, powiedzieli, że żałują, że dostarczyli API jako interfejs. Nie słyszałem o żadnym przypadku, w którym ktoś żałował, że wysłał klasę.

Biorąc to pod uwagę, z pewnością jest miejsce na interfejsy. Jako ogólną wytyczną zawsze należy podać abstrakcyjną implementację interfejsu klasy podstawowej, jeśli nie ma innego przykładu jako przykład sposobu implementacji interfejsu. W najlepszym przypadku ta klasa podstawowa pozwoli zaoszczędzić dużo pracy.


19

Juan,

Lubię myśleć o interfejsach jako sposobie charakteryzowania klasy. Określona klasa ras psów, powiedzmy YorkshireTerrier, może być potomkiem klasy psów nadrzędnych, ale implementuje także IFurry, IStubby i IYippieDog. Tak więc klasa określa, czym jest klasa, ale interfejs mówi nam o tym.

Zaletą tego jest to, że mogę na przykład zebrać wszystkie IYippieDog i wrzucić je do mojej kolekcji Ocean. Teraz mogę sięgnąć po określony zestaw obiektów i znaleźć te, które spełniają kryteria, na które patrzę, bez szczegółowej inspekcji klasy.

Uważam, że interfejsy naprawdę powinny definiować podzbiór publicznego zachowania klasy. Jeśli określa wszystkie zachowania publiczne dla wszystkich klas, które implementują, to zwykle nie musi istnieć. Nic mi nie mówią.

Ta myśl jest jednak sprzeczna z ideą, że każda klasa powinna mieć interfejs i należy do niego kodować. W porządku, ale kończy się wiele interfejsów jeden do jednego do klas i to sprawia, że ​​wszystko jest mylące. Rozumiem, że pomysł jest taki, że tak naprawdę nic nie kosztuje, a teraz możesz z łatwością wymieniać różne elementy. Uważam jednak, że rzadko to robię. Przez większość czasu po prostu modyfikuję istniejącą klasę i mam dokładnie takie same problemy, jakie zawsze występowałem, jeśli interfejs publiczny tej klasy wymaga zmiany, z tym że muszę go teraz zmienić w dwóch miejscach.

Więc jeśli myślisz tak jak ja, zdecydowanie powiedziałbyś, że Kot i Pies są IPettable. Jest to charakterystyka, która pasuje do nich obu.

Inną kwestią jest to, czy powinny mieć tę samą klasę podstawową? Pytanie brzmi, czy należy je ogólnie traktować tak samo. Z pewnością są to Zwierzęta, ale czy to pasuje do tego, w jaki sposób będziemy je razem wykorzystywać.

Powiedz, że chcę zebrać wszystkie klasy zwierząt i umieścić je w moim pojemniku na Arkę.

A może muszą to być ssaki? Być może potrzebujemy jakiejś fabryki doju krzyżowego?

Czy w ogóle muszą być ze sobą powiązane? Czy wystarczy wiedzieć, że oba są IPettable?

Często odczuwam chęć wyprowadzenia całej hierarchii klas, kiedy naprawdę potrzebuję tylko jednej klasy. Robię to w oczekiwaniu, że kiedyś będę tego potrzebować i zwykle nigdy tego nie robię. Nawet kiedy to robię, zwykle muszę dużo zrobić, aby to naprawić. To dlatego, że pierwszą klasą, którą tworzę, nie jest Pies, nie mam tyle szczęścia, tylko Dziobak. Teraz cała moja hierarchia klas oparta jest na dziwnym przypadku i mam dużo zmarnowanego kodu.

W pewnym momencie możesz również stwierdzić, że nie wszystkie Koty są IPettable (jak ten bezwłosy). Teraz możesz przenieść ten interfejs do wszystkich pasujących klas pochodnych. Przekonasz się, że o wiele mniej przełomowa zmiana, że ​​nagle wszystkie Koty nie są już uzyskiwane z Bazy PettableBase.


18

Oto podstawowa i prosta definicja interfejsu i klasy bazowej:

  • Klasa podstawowa = dziedziczenie obiektu.
  • Interfejs = dziedziczenie funkcjonalne.

Twoje zdrowie


12

W miarę możliwości zalecam stosowanie kompozycji zamiast dziedziczenia. Używaj interfejsów, ale do implementacji podstawowej używaj obiektów członkowskich. W ten sposób możesz zdefiniować fabrykę, która konstruuje twoje obiekty tak, by zachowywały się w określony sposób. Jeśli chcesz zmienić to zachowanie, wówczas tworzysz nową metodę fabryczną (lub fabrykę abstrakcyjną), która tworzy różne typy podobiektów.

W niektórych przypadkach może się okazać, że twoje podstawowe obiekty wcale nie potrzebują interfejsów, jeśli wszystkie zmienne zachowania są zdefiniowane w obiektach pomocniczych.

Dlatego zamiast IPet lub PetBase możesz skończyć ze zwierzakiem, który ma parametr IFurBehavior. Parametr IFurBehavior jest ustawiany przez metodę CreateDog () PetFactory. Ten parametr jest wywoływany dla metody shed ().

Jeśli to zrobisz, zauważysz, że kod jest znacznie bardziej elastyczny, a większość prostych obiektów radzi sobie z bardzo podstawowymi zachowaniami w całym systemie.

Polecam ten wzorzec nawet w językach wielokrotnego dziedziczenia.


12

Zostało to dobrze wyjaśnione w tym artykule na temat Java World .

Osobiście używam interfejsów do definiowania interfejsów - tj. Części projektu systemu, które określają, w jaki sposób należy uzyskać dostęp.

Nierzadko będę miał klasę implementującą jeden lub więcej interfejsów.

Klasy abstrakcyjne, których używam jako podstawy do czegoś innego.

Poniżej znajduje się wyciąg z wyżej wymienionego artykułu Artykuł JavaWorld.com, autor Tony Sintes, 04/20/01


Interfejs a klasa abstrakcyjna

Wybór interfejsów i klas abstrakcyjnych nie jest ani propozycją. Jeśli potrzebujesz zmienić swój projekt, uczyń go interfejsem. Jednak mogą istnieć klasy abstrakcyjne, które zapewniają pewne domyślne zachowanie. Klasy abstrakcyjne są doskonałymi kandydatami w ramach aplikacji.

Klasy abstrakcyjne pozwalają zdefiniować niektóre zachowania; zmuszają twoje podklasy do zapewniania innych. Na przykład, jeśli masz strukturę aplikacji, klasa abstrakcyjna może zapewniać domyślne usługi, takie jak obsługa zdarzeń i komunikatów. Usługi te pozwalają Twojej aplikacji na podłączenie się do środowiska aplikacji. Istnieją jednak funkcje specyficzne dla aplikacji, które może wykonać tylko Twoja aplikacja. Taka funkcjonalność może obejmować zadania uruchamiania i zamykania, które często zależą od aplikacji. Zamiast więc próbować zdefiniować to zachowanie, abstrakcyjna klasa bazowa może zadeklarować abstrakcyjne metody zamykania i uruchamiania. Klasa podstawowa wie, że potrzebuje tych metod, ale klasa abstrakcyjna pozwala twojej klasie przyznać, że nie wie, jak wykonać te akcje; wie tylko, że musi inicjować działania. Kiedy nadejdzie czas na uruchomienie, klasa abstrakcyjna może wywoływać metodę uruchamiania. Gdy klasa podstawowa wywołuje tę metodę, Java wywołuje metodę zdefiniowaną przez klasę potomną.

Wielu programistów zapomina, że ​​klasa, która definiuje metodę abstrakcyjną, może również wywołać tę metodę. Klasy abstrakcyjne to doskonały sposób na tworzenie planowanych hierarchii dziedziczenia. Są również dobrym wyborem dla klas bez liści w hierarchiach klas.

Klasa vs. interfejs

Niektórzy twierdzą, że należy zdefiniować wszystkie klasy w kategoriach interfejsów, ale myślę, że zalecenie wydaje się nieco ekstremalne. Korzystam z interfejsów, gdy widzę, że coś w moim projekcie często się zmienia.

Na przykład wzorzec strategii pozwala zamieniać nowe algorytmy i procesy w programie bez zmiany obiektów, które ich używają. Odtwarzacz multimedialny może wiedzieć, jak odtwarzać dyski CD, MP3 i WAV. Oczywiście nie chcesz na stałe kodować tych algorytmów odtwarzania w odtwarzaczu; utrudni to dodanie nowego formatu, takiego jak AVI. Co więcej, twój kod będzie pełen niepotrzebnych instrukcji case. Aby dodać zniewagę do obrażeń, będziesz musiał aktualizować te oświadczenia o sprawach za każdym razem, gdy dodasz nowy algorytm. Podsumowując, nie jest to zbyt obiektowy sposób programowania.

Za pomocą wzorca strategii możesz po prostu obudować algorytm za obiektem. Jeśli to zrobisz, możesz w dowolnym momencie udostępnić nowe wtyczki multimedialne. Nazwijmy klasę wtyczek MediaStrategy. Ten obiekt miałby jedną metodę: playStream (Stream). Aby dodać nowy algorytm, po prostu rozszerzamy naszą klasę algorytmów. Teraz, gdy program napotka nowy typ mediów, po prostu przekazuje odtwarzanie strumienia do naszej strategii medialnej. Oczywiście będziesz potrzebować trochę hydrauliki, aby poprawnie utworzyć potrzebne strategie algorytmów.

To doskonałe miejsce do korzystania z interfejsu. Zastosowaliśmy wzór strategii, który wyraźnie wskazuje miejsce w projekcie, które się zmieni. Dlatego należy zdefiniować strategię jako interfejs. Powinieneś ogólnie preferować interfejsy zamiast dziedziczenia, jeśli chcesz, aby obiekt miał określony typ; w tym przypadku MediaStrategy. Poleganie na dziedziczeniu tożsamości typu jest niebezpieczne; blokuje cię w określonej hierarchii dziedziczenia. Java nie pozwala na wielokrotne dziedziczenie, więc nie można rozszerzyć czegoś, co daje użyteczną implementację lub więcej typów tożsamości.


2
+1. „Poleganie na dziedziczeniu tożsamości typu jest niebezpieczne; blokuje cię w określonej hierarchii dziedziczenia”. To zdanie doskonale opisuje moje powody, dla których preferuję interfejsy.
Inżynier

Poza tym, zamiast rozszerzania, utwórz implementację każdej metody twojego interfejsu.
Inżynier

10

Pamiętaj również, aby nie zostać zmiecionym z OO ( patrz blog ) i zawsze modeluj obiekty w oparciu o wymagane zachowanie, jeśli projektujesz aplikację, w której jedynym wymaganym zachowaniem jest ogólna nazwa i gatunek zwierzęcia, potrzebujesz tylko jedna klasa Zwierzę z właściwością dla nazwy, zamiast milionów klas dla każdego możliwego zwierzęcia na świecie.


10

Mam szorstką zasadę

Funkcjonalność: prawdopodobnie będzie różna we wszystkich częściach: Interfejs.

Dane i funkcje, części będą w większości takie same, części inne: klasa abstrakcyjna.

Dane i funkcjonalność, faktycznie działające, jeśli zostaną rozszerzone tylko z niewielkimi zmianami: klasa zwykła (konkretna)

Dane i funkcjonalność, bez planowanych zmian: zwykła (konkretna) klasa z ostatecznym modyfikatorem.

Dane, a może i funkcjonalność: tylko do odczytu: członkowie enum.

Jest to bardzo szorstkie i gotowe i wcale nie jest ściśle określone, ale istnieje spektrum od interfejsów, w których wszystko ma być zmienione, do wyliczeń, w których wszystko jest ustawione trochę jak plik tylko do odczytu.


7

Interfejsy powinny być małe. Bardzo mały. Jeśli naprawdę rozbijasz swoje obiekty, wówczas interfejsy prawdopodobnie będą zawierały tylko kilka bardzo specyficznych metod i właściwości.

Klasy abstrakcyjne to skróty. Czy są rzeczy, które współużytkują wszystkie pochodne PetBase, które możesz zakodować raz i zrobić z nimi? Jeśli tak, to czas na abstrakcyjną klasę.

Ograniczają się także klasy abstrakcyjne. Mimo że stanowią doskonały skrót do tworzenia obiektów potomnych, każdy dany obiekt może implementować tylko jedną klasę abstrakcyjną. Wiele razy uważam to za ograniczenie klas abstrakcyjnych i dlatego używam wielu interfejsów.

Klasy abstrakcyjne mogą zawierać kilka interfejsów. Klasa abstrakcyjna PetBase może implementować IPet (zwierzęta mają właścicieli) i niestrawność (zwierzęta jedzą, a przynajmniej powinny). Jednak PetBase prawdopodobnie nie wdroży IMammal, ponieważ nie wszystkie zwierzęta są ssakami i nie wszystkie ssaki są zwierzętami domowymi. Możesz dodać MammalPetBase, który rozszerza PetBase i dodać IMammal. FishBase może mieć PetBase i dodać IFish. IFish miałby ISwim i IUnderwaterBreather jako interfejsy.

Tak, mój przykład jest nadmiernie skomplikowany w stosunku do prostego przykładu, ale to część wspaniałej rzeczy na temat tego, jak interfejsy i klasy abstrakcyjne działają razem.


7

Źródło : http://jasonroell.com/2014/12/09/interfaces-vs-abstract-classes-what-should-you-use/

C # to wspaniały język, który dojrzał i ewoluował w ciągu ostatnich 14 lat. Jest to świetne dla nas, programistów, ponieważ dojrzały język zapewnia nam mnóstwo funkcji językowych, które są do naszej dyspozycji.

Jednak z dużą mocą staje się duża odpowiedzialność. Niektóre z tych funkcji mogą być niewłaściwie używane, a czasem trudno jest zrozumieć, dlaczego zdecydujesz się korzystać z jednej funkcji nad drugą. Przez lata funkcja, z którą widziałem wielu programistów, polega na tym, kiedy wybrać interfejs lub klasę abstrakcyjną. Oba mają wady i zalety, a każdy z nich ma odpowiedni czas i miejsce. Ale jak zdecydować?

Oba zapewniają ponowne wykorzystanie wspólnej funkcjonalności między typami. Najbardziej oczywistą różnicą jest to, że interfejsy nie zapewniają żadnej implementacji dla ich funkcjonalności, podczas gdy klasy abstrakcyjne pozwalają na implementację niektórych „podstawowych” lub „domyślnych” zachowań, a następnie mają możliwość „zastąpienia” tego domyślnego zachowania typami pochodnymi klas, jeśli to konieczne .

Wszystko to dobrze i dobrze, zapewnia świetne ponowne użycie kodu i przestrzega zasady DRY (Don't Repeat Yourself) tworzenia oprogramowania. Klasy abstrakcyjne są świetne do użycia, gdy masz relację „jest”.

Na przykład: Golden retriever „jest” typem psa. Pudel też. Oboje mogą szczekać, tak jak wszystkie psy. Możesz jednak stwierdzić, że park pudli różni się znacznie od „domyślnej” kory psa. Dlatego warto wdrożyć coś w następujący sposób:

public abstract class Dog
{
      public virtual void Bark()
      {
        Console.WriteLine("Base Class implementation of Bark");
      }
}

public class GoldenRetriever : Dog
{
   // the Bark method is inherited from the Dog class
}

public class Poodle : Dog
{
  // here we are overriding the base functionality of Bark with our new implementation
  // specific to the Poodle class
  public override void Bark()
  {
     Console.WriteLine("Poodle's implementation of Bark");
  }
}

// Add a list of dogs to a collection and call the bark method.

void Main()
{
    var poodle = new Poodle();
    var goldenRetriever = new GoldenRetriever();

    var dogs = new List<Dog>();
    dogs.Add(poodle);
    dogs.Add(goldenRetriever);

    foreach (var dog in dogs)
    {
       dog.Bark();
    }
}

// Output will be:
// Poodle's implementation of Bark
// Base Class implementation of Bark

// 

Jak widać, byłby to świetny sposób na zachowanie stanu DRY w kodzie i umożliwienia wywoływania implementacji klasy podstawowej, gdy dowolny z typów może polegać tylko na domyślnej Bark zamiast implementacji specjalnego przypadku. Klasy takie jak GoldenRetriever, Boxer, Lab mogą wszyscy odziedziczyć „domyślną” (klasę basu) Kora bez żadnych opłat tylko dlatego, że implementują klasę abstrakcyjną Dog.

Ale jestem pewien, że już to wiedziałeś.

Jesteś tutaj, ponieważ chcesz zrozumieć, dlaczego możesz chcieć wybrać interfejs zamiast klasy abstrakcyjnej lub odwrotnie. Cóż, jednym z powodów, dla których możesz chcieć wybrać interfejs zamiast klasy abstrakcyjnej jest to, że nie masz lub chcesz zapobiec domyślnej implementacji. Wynika to zwykle z tego, że typy, które implementują interfejs niezwiązany w relacji „jest”. W rzeczywistości nie muszą być w ogóle powiązane, z wyjątkiem faktu, że każdy typ „jest w stanie” lub ma „zdolność” do zrobienia czegoś lub posiadania czegoś.

Co to do cholery znaczy? Na przykład: człowiek nie jest kaczką… a kaczka nie jest człowiekiem. Dość oczywiste. Jednak zarówno kaczka, jak i człowiek mają „zdolność” pływania (biorąc pod uwagę, że człowiek zdał lekcje pływania w 1. klasie :)). Ponadto, ponieważ kaczka nie jest człowiekiem lub odwrotnie, nie jest to realizacja „jest”, ale relacja „jest w stanie”, a my możemy użyć interfejsu do zilustrowania tego:

// Create ISwimable interface
public interface ISwimable
{
      public void Swim();
}

// Have Human implement ISwimable Interface
public class Human : ISwimable

     public void Swim()
     {
        //Human's implementation of Swim
        Console.WriteLine("I'm a human swimming!");
     }

// Have Duck implement ISwimable interface
public class Duck: ISwimable
{
     public void Swim()
     {
          // Duck's implementation of Swim
          Console.WriteLine("Quack! Quack! I'm a Duck swimming!")
     }
}

//Now they can both be used in places where you just need an object that has the ability "to swim"

public void ShowHowYouSwim(ISwimable somethingThatCanSwim)
{
     somethingThatCanSwim.Swim();
}

public void Main()
{
      var human = new Human();
      var duck = new Duck();

      var listOfThingsThatCanSwim = new List<ISwimable>();

      listOfThingsThatCanSwim.Add(duck);
      listOfThingsThatCanSwim.Add(human);

      foreach (var something in listOfThingsThatCanSwim)
      {
           ShowHowYouSwim(something);
      }
}

 // So at runtime the correct implementation of something.Swim() will be called
 // Output:
 // Quack! Quack! I'm a Duck swimming!
 // I'm a human swimming!

Korzystanie z interfejsów takich jak powyższy kod pozwoli ci przekazać obiekt do metody, która „jest w stanie” coś zrobić. Kod nie dba o to, jak to robi… Wie tylko, że może wywoływać metodę Swim na tym obiekcie, a ten obiekt będzie wiedział, jakie zachowanie podejmie w czasie wykonywania na podstawie jego typu.

Po raz kolejny pomaga to Twojemu kodowi pozostać SUCHYM, dzięki czemu nie będziesz musiał pisać wielu metod, które wywołują obiekt, aby wykonać tę samą funkcję podstawową (ShowHowHumanSwims (human), ShowHowDuckSwims (duck) itp.)

Korzystanie z interfejsu w tym miejscu pozwala metodom wywoływania nie martwić się o to, jaki typ jest który lub w jaki sposób zachowanie jest realizowane. Po prostu wie, że biorąc pod uwagę interfejs, każdy obiekt będzie musiał zaimplementować metodę Swim, więc można bezpiecznie wywołać go we własnym kodzie i pozwolić, aby zachowanie metody Swim było obsługiwane w ramach jego własnej klasy.

Podsumowanie:

Więc moją główną zasadą jest użycie klasy abstrakcyjnej, gdy chcesz zaimplementować funkcję „domyślną” dla hierarchii klas lub / i klasy lub typy, z którymi pracujesz, mają relację „jest” (np. Pudel „jest ”Typ psa).

Z drugiej strony używaj interfejsu, gdy nie masz relacji „jest”, ale masz typy, które dzielą „zdolność” do zrobienia czegoś lub posiadania czegoś (np. Kaczka „nie jest” człowiekiem. Jednak kaczka i człowiek dzielą się „Umiejętność” pływania).

Inną różnicą, na którą należy zwrócić uwagę między klasami abstrakcyjnymi a interfejsami, jest to, że klasa może implementować jeden lub wiele interfejsów, ale klasa może dziedziczyć tylko z JEDNEJ klasy abstrakcyjnej (lub dowolnej innej klasy pod tym względem). Tak, możesz zagnieżdżać klasy i mieć hierarchię dziedziczenia (którą wiele programów robi i powinno mieć), ale nie możesz dziedziczyć dwóch klas w jednej definicji klasy pochodnej (ta reguła dotyczy C #. W niektórych innych językach możesz to zrobić, zwykle tylko z powodu braku interfejsów w tych językach).

Pamiętaj także o korzystaniu z interfejsów w celu przestrzegania zasady segregacji interfejsów (ISP). ISP stwierdza, że ​​żaden klient nie powinien być zmuszany do polegania na metodach, których nie używa. Z tego powodu interfejsy powinny koncentrować się na konkretnych zadaniach i zwykle są bardzo małe (np. IDisposable, IComparable).

Inną wskazówką jest, jeśli opracowujesz małe, zwięzłe elementy funkcjonalności, korzystaj z interfejsów. Jeśli projektujesz duże jednostki funkcjonalne, użyj klasy abstrakcyjnej.

Mam nadzieję, że to wyjaśni niektóre osoby!

Również jeśli możesz wymyślić jakieś lepsze przykłady lub chcesz coś wskazać, zrób to w komentarzach poniżej!


6

Sprawa dla klas podstawowych nad interfejsami została dobrze wyjaśniona w Podmain .NET Coding Guidelines:

Klasy podstawowe a interfejsy Typ interfejsu to częściowy opis wartości, potencjalnie obsługiwany przez wiele typów obiektów. W miarę możliwości używaj klas podstawowych zamiast interfejsów. Z perspektywy wersjonowania klasy są bardziej elastyczne niż interfejsy. Za pomocą klasy możesz wysłać wersję 1.0, a następnie w wersji 2.0 dodać nową metodę do klasy. Tak długo, jak metoda nie jest abstrakcyjna, wszelkie istniejące klasy pochodne nadal działają bez zmian.

Ponieważ interfejsy nie obsługują dziedziczenia implementacji, wzorzec mający zastosowanie do klas nie ma zastosowania do interfejsów. Dodanie metody do interfejsu jest równoważne z dodaniem metody abstrakcyjnej do klasy podstawowej; każda klasa implementująca interfejs ulegnie awarii, ponieważ klasa nie implementuje nowej metody. Interfejsy są odpowiednie w następujących sytuacjach:

  1. Kilka niepowiązanych klas chce obsługiwać protokół.
  2. Klasy te już ustanowiły klasy podstawowe (na przykład niektóre są kontrolkami interfejsu użytkownika, a niektóre są usługami XML sieci Web).
  3. Agregacja nie jest właściwa ani praktyczna. We wszystkich innych sytuacjach dziedziczenie klas jest lepszym modelem.

Myślę, że ta odpowiedź powinna zyskać więcej uwagi. Jest to sprzeczne z wieloma odpowiedziami tutaj. Nie powiedziałbym, że całkowicie się zgadzam, ale tutaj są świetne punkty.
kayleeFrye_onDeck

5

Jedną ważną różnicą jest to, że możesz odziedziczyć tylko jedną klasę bazową, ale możesz zaimplementować wiele interfejsów. Dlatego chcesz użyć klasy podstawowej tylko wtedy, gdy masz absolutną pewność, że nie będziesz musiał dziedziczyć innej klasy podstawowej. Dodatkowo, jeśli okaże się, że twój interfejs robi się duży, powinieneś zacząć rozbijać go na kilka logicznych elementów, które definiują niezależną funkcjonalność, ponieważ nie ma reguły, że twoja klasa nie może zaimplementować ich wszystkich (lub że możesz zdefiniować inny interfejs, który po prostu dziedziczy je wszystkie, aby je grupować).


4

Kiedy zacząłem uczyć się o programowaniu obiektowym, popełniłem łatwy i prawdopodobnie powszechny błąd, używając dziedziczenia do dzielenia się wspólnym zachowaniem - nawet jeśli takie zachowanie nie było istotne dla natury obiektu.

Aby dalej opierać się na przykładzie często używanym w tym konkretnym pytaniu, istnieje wiele rzeczy, które można karmić - dziewczyny, samochody, puszyste koce ... - więc mógłbym mieć klasę Petable, która zapewniła to wspólne zachowanie i różne klasy dziedziczące z tego.

Jednak bycie zwierzęciem nie należy do natury żadnego z tych przedmiotów. Istnieją znacznie ważniejsze pojęcia, które istotne dla ich natury - dziewczyna to osoba, samochód to pojazd lądowy, kot to ssak ...

Zachowania należy przypisać najpierw do interfejsów (w tym domyślnego interfejsu klasy) i awansować do klasy podstawowej tylko wtedy, gdy są one (a) wspólne dla dużej grupy klas, które są podzbiorami większej klasy - w tym samym sensie, że „kot” i „osoba” to podzbiory „ssaka”.

Problem polega na tym, że po zrozumieniu projektowania obiektowego wystarczająco lepiej niż ja na początku zwykle robisz to automatycznie, nawet o tym nie myśląc. Tak więc sama prawda o stwierdzeniu „kod do interfejsu, a nie klasa abstrakcyjna” staje się tak oczywista, że ​​trudno ci uwierzyć, że ktoś zechce to powiedzieć - i zaczniesz odczytywać w niej inne znaczenia.

Inną rzeczą, którą dodam, jest to, że jeśli klasa jest czysto abstrakcyjna - bez żadnych nieabstrakcyjnych, nie odziedziczonych członków lub metod narażonych na dziecko, rodzica lub klienta - to dlaczego jest to klasa? Może być zastąpiony, w niektórych przypadkach interfejsem, aw innych przez Null.


Klasa czysto abstrakcyjna może zapewnić domyślne zachowanie metod. Jest to przydatne, gdy wszystkie twoje konkretne klasy będą miały wspólne metody, które byłyby zbędne w celu ponownego wdrożenia w kółko.
Adam Hughes,

4

Wolę interfejsy niż klasy abstrakcyjne

Uzasadnienie, główne punkty do rozważenia [dwa już tutaj wspomniane] to:

  • Interfejsy są bardziej elastyczne, ponieważ klasa może implementować wiele interfejsów. Ponieważ Java nie ma wielokrotnego dziedziczenia, użycie klas abstrakcyjnych uniemożliwia użytkownikom korzystanie z jakiejkolwiek innej hierarchii klas. Ogólnie rzecz biorąc, preferuj interfejsy, gdy nie ma domyślnych implementacji lub stanu. Kolekcje Java oferują dobre przykłady tego (mapa, zestaw itp.).
  • Klasy abstrakcyjne mają tę zaletę, że umożliwiają lepszą kompatybilność w przód. Gdy klienci korzystają z interfejsu, nie można go zmienić; jeśli używają klasy abstrakcyjnej, nadal można dodać zachowanie bez przerywania istniejącego kodu. Jeśli problem dotyczy kompatybilności, rozważ użycie klas abstrakcyjnych.
  • Nawet jeśli masz domyślne implementacje lub stan wewnętrzny, rozważ zaoferowanie interfejsu i jego abstrakcyjnej implementacji . Pomoże to klientom, ale nadal zapewni im większą swobodę w razie potrzeby [1].
    Oczywiście temat ten został obszernie omówiony w innym miejscu [2,3].

[1] Oczywiście dodaje więcej kodu, ale jeśli zwięzłość jest twoją główną troską, prawdopodobnie powinieneś był unikać Javy!

[2] Joshua Bloch, Effective Java, pozycje 16–18.

[3] http://www.codeproject.com/KB/ar ...


3

Poprzednie komentarze na temat używania klas abstrakcyjnych do wspólnej implementacji są zdecydowanie na znak. Jedną z korzyści, o których jeszcze nie wspomniałem, jest to, że użycie interfejsów znacznie ułatwia implementację próbnych obiektów do celów testowania jednostkowego. Zdefiniowanie IPet i PetBase zgodnie z opisem Jason Cohen pozwala łatwo wyśmiewać różne warunki danych, bez obciążania fizycznej bazy danych (dopóki nie zdecydujesz, że nadszedł czas, aby przetestować prawdziwą rzecz).


3

Nie używaj klasy bazowej, chyba że wiesz, co to znaczy i że ma ona zastosowanie w tym przypadku. Jeśli ma to zastosowanie, użyj go, w przeciwnym razie użyj interfejsów. Zwróć jednak uwagę na odpowiedź dotyczącą małych interfejsów.

Dziedziczenie publiczne jest nadużywane w OOD i wyraża o wiele więcej, niż większość programistów zdaje sobie sprawę lub jest gotowa sprostać. Zobacz zasadę substytucyjności Liskowa

Krótko mówiąc, jeśli A „jest” B, to A wymaga nie więcej niż B i dostarcza nie mniej niż B, dla każdej metody, którą eksponuje.


3

Inną opcją, o której należy pamiętać, jest użycie relacji „has-a”, aka ”, realizowanej w kategoriach„ lub ”kompozycji. Czasami jest to czystszy, bardziej elastyczny sposób na uporządkowanie rzeczy niż używanie dziedziczenia „jest”.

Logiczne może być stwierdzenie, że zarówno Pies, jak i Kot „mają” zwierzaka, ale pozwala uniknąć typowych pułapek wielokrotnego dziedziczenia:

public class Pet
{
    void Bathe();
    void Train(Trick t);
}

public class Dog
{
    private Pet pet;

    public void Bathe() { pet.Bathe(); }
    public void Train(Trick t) { pet.Train(t); }
}

public class Cat
{
    private Pet pet;

    public void Bathe() { pet.Bathe(); }
    public void Train(Trick t) { pet.Train(t); }
}

Tak, ten przykład pokazuje, że w wykonywaniu tych czynności jest dużo duplikacji kodu i braku elegancji. Ale należy również docenić, że pomaga to utrzymać oddzielenie Psa i Kota od klasy Pet (w tym, że Pies i Kot nie mają dostępu do prywatnych członków Pet) i pozostawia miejsce dla Psa i Kota na dziedziczenie po czymś innym - -prawdopodobnie klasa Mammal.

Kompozycja jest preferowana, gdy nie jest wymagany dostęp prywatny i nie trzeba odwoływać się do psów i kotów za pomocą ogólnych odniesień / wskaźników dla zwierząt domowych. Interfejsy zapewniają tę ogólną funkcję odniesienia i mogą pomóc w ograniczeniu szczegółowości kodu, ale mogą również zaciemniać rzeczy, gdy są źle zorganizowane. Dziedziczenie jest przydatne, gdy potrzebujesz dostępu do prywatnego członka, a korzystając z niego, zobowiązujesz się do silnego połączenia klas psów i kotów z klasą zwierząt domowych, co jest stratą.

Pomiędzy dziedziczeniem, kompozycją i interfejsami nie ma jednego sposobu, który jest zawsze właściwy, i pomaga zastanowić się, w jaki sposób wszystkie trzy opcje mogą być używane w harmonii. Spośród tych trzech elementów dziedziczenie jest zazwyczaj opcją, z której należy korzystać najrzadziej.


3

Pod względem koncepcyjnym interfejs służy do formalnego i półformalnego zdefiniowania zestawu metod, które zapewni obiekt. Formalnie oznacza zbiór nazw i podpisów metod, a półformalnie oznacza czytelną dla ludzi dokumentację związaną z tymi metodami.

Interfejsy są jedynie opisami API (w końcu API oznacza interfejs programowania aplikacji ), nie mogą zawierać żadnej implementacji i nie można używać ani uruchamiać interfejsu. Zawierają jedynie wyraźną umowę dotyczącą sposobu interakcji z przedmiotem.

Klasy zapewniają implementację i mogą zadeklarować, że implementują zero, jeden lub więcej interfejsów. Jeśli klasa ma być dziedziczona, konwencja polega na prefiksie nazwy klasy „Base”.

Istnieje różnica między klasą podstawową a abstrakcyjną klasą podstawową (ABC). ABC łączy interfejs i implementację razem. Streszczenie poza programowaniem komputerowym oznacza „streszczenie”, czyli „abstract == interface”. Abstrakcyjna klasa bazowa może następnie opisać zarówno interfejs, jak i pusty, częściowego lub całkowitego wdrożenia, który jest przeznaczony do dziedziczone.

Opinie na temat tego, kiedy używać interfejsów w porównaniu z abstrakcyjnymi klasami podstawowymi w porównaniu do zwykłych klas będą się bardzo różnić w zależności od tego, co opracowujesz, i języka, w którym się rozwijasz. Interfejsy są często kojarzone tylko z językami o typie statycznym, takim jak Java lub C #, ale dynamicznie pisane języki mogą mieć również interfejsy i abstrakcyjne klasy podstawowe . Python na przykład rozróżnienie pusta pomiędzy klasy, co stwierdzi, że realizuje się interfejs i obiektu, który jest wystąpienie klasy i mówi się, że zapewniają , że interfejs. W dynamicznym języku jest możliwe, że dwa obiekty, które są instancjami tej samej klasy , mogą zadeklarować, że zapewniają zupełnie inne interfejsy. W Pythonie jest to możliwe tylko w przypadku atrybutów obiektu, podczas gdy metody są współużytkowane przez wszystkie obiekty klasy . Jednak w Ruby obiekty mogą mieć metody dla poszczególnych instancji, więc możliwe jest, że interfejs między dwoma obiektami tej samej klasy może się różnić tak bardzo, jak chce programista (jednak Ruby nie ma wyraźnego sposobu deklarowania interfejsów).

W językach dynamicznych interfejs do obiektu jest często domyślnie zakładany, albo przez introspekcję obiektu i zapytanie go, jakie zapewnia metody ( spójrz przed skokiem ), albo najlepiej po prostu przez użycie pożądanego interfejsu na obiekcie i wyłapanie wyjątków, jeśli obiekt nie zapewnia tego interfejsu ( łatwiej prosić o wybaczenie niż pozwolenie ). Może to prowadzić do „fałszywych trafień”, w których dwa interfejsy mają tę samą nazwę metody, ale są semantycznie różne. Jednak kompromis polega na tym, że kod jest bardziej elastyczny, ponieważ nie trzeba przesadnie określać z góry, aby przewidzieć wszystkie możliwe zastosowania kodu.


2

To zależy od twoich wymagań. Jeśli IPet jest dość prosty, wolałbym to zaimplementować. W przeciwnym razie, jeśli PetBase zaimplementuje mnóstwo funkcji, których nie chcesz powielać, skorzystaj z niego.

Minusem zaimplementowania klasy podstawowej jest wymóg override(lub new) istniejących metod. To czyni je wirtualnymi metodami, co oznacza, że ​​musisz uważać na sposób korzystania z instancji obiektu.

Wreszcie, jedno dziedzictwo .NET mnie zabija. Naiwny przykład: powiedz, że kontrolujesz użytkownika, więc dziedziczysz UserControl. Ale teraz nie możesz też dziedziczyć PetBase. Zmusza to do reorganizacji, na przykład do PetBaseczłonkostwa w klasie.


2

Zazwyczaj też nie wdrażam, dopóki go nie potrzebuję. Preferuję interfejsy zamiast klas abstrakcyjnych, ponieważ daje to nieco większą elastyczność. Jeśli w niektórych dziedziczących klasach występuje wspólne zachowanie, przesuwam je w górę i tworzę abstrakcyjną klasę podstawową. Nie widzę potrzeby korzystania z obu rozwiązań, ponieważ zasadniczo służą one temu samemu celowi, a posiadanie obu jest nieprzyjemnym zapachem kodu (imho), że rozwiązanie zostało przeprojektowane.


2

Jeśli chodzi o C #, w pewnym sensie interfejsy i klasy abstrakcyjne mogą być wymienne. Różnice są jednak następujące: i) interfejsy nie mogą implementować kodu; ii) z tego powodu interfejsy nie mogą wywoływać dalej stosu do podklasy; oraz iii) tylko klasa abstrakcyjna może być dziedziczona w klasie, podczas gdy wiele interfejsów może być implementowanych w klasie.


2

Z definicji interfejs zapewnia warstwę do komunikacji z innym kodem. Wszystkie publiczne właściwości i metody klasy domyślnie implementują niejawny interfejs. Możemy również zdefiniować interfejs jako rolę, gdy kiedykolwiek jakaś klasa musi odgrywać tę rolę, musi go wdrożyć, dając mu różne formy implementacji w zależności od klasy, która go implementuje. Dlatego kiedy mówisz o interfejsie, mówisz o polimorfizmie, a kiedy mówisz o klasie podstawowej, mówisz o dziedziczeniu. Dwie koncepcje ups!


2

Odkryłem, że wzór Interfejs> Streszczenie> Beton działa w następującym przypadku użycia:

1.  You have a general interface (eg IPet)
2.  You have a implementation that is less general (eg Mammal)
3.  You have many concrete members (eg Cat, Dog, Ape)

Klasa abstrakcyjna definiuje domyślne wspólne atrybuty klas konkretnych, ale wymusza interfejs. Na przykład:

public interface IPet{

    public boolean hasHair();

    public boolean walksUprights();

    public boolean hasNipples();
}

Teraz, ponieważ wszystkie ssaki mają włosy i sutki (AFAIK, nie jestem zoologiem), możemy przenieść to do abstrakcyjnej klasy podstawowej

public abstract class Mammal() implements IPet{

     @override
     public walksUpright(){
         throw new NotSupportedException("Walks Upright not implemented");
     }

     @override
     public hasNipples(){return true}

     @override
     public hasHair(){return true}

A potem konkretne klasy po prostu definiują, że idą w pozycji pionowej.

public class Ape extends Mammal(){

    @override
    public walksUpright(return true)
}

public class Catextends Mammal(){

    @override
    public walksUpright(return false)
}

Ta konstrukcja jest dobra, gdy istnieje wiele konkretnych klas i nie chcesz utrzymywać płyty kotłowej tylko w celu zaprogramowania interfejsu. Gdyby nowe metody zostały dodane do interfejsu, spowodowałoby to uszkodzenie wszystkich klas wynikowych, więc nadal zyskujesz zalety podejścia opartego na interfejsie.

W tym przypadku streszczenie równie dobrze może być konkretne; jednak abstrakcyjne oznaczenie pomaga podkreślić, że ten wzór jest stosowany.


1

Dziedzicznik klasy podstawowej powinien mieć relację „jest”. Interfejs reprezentuje An „implementuje” relację. Dlatego używaj klasy podstawowej tylko wtedy, gdy spadkobiercy utrzymają relację.


1

Użyj Interfejsów, aby wyegzekwować umowę ACROSS rodzin niezwiązanych klas. Na przykład możesz mieć wspólne metody dostępu dla klas reprezentujących kolekcje, ale zawierające radykalnie różne dane, tj. Jedna klasa może reprezentować zestaw wyników z zapytania, a druga może reprezentować obrazy w galerii. Ponadto można zaimplementować wiele interfejsów, co pozwala łączyć (i oznaczać) możliwości klasy.

Użyj Dziedziczenia, gdy klasy mają wspólny związek, a zatem mają podobną sygnaturę strukturalną i behawioralną, tj. Samochód, Motocykl, Ciężarówka i SUV to wszystkie rodzaje pojazdów drogowych, które mogą zawierać wiele kół, maksymalną prędkość

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.