Mój kolega wymyślił ogólną zasadę wyboru między tworzeniem klasy podstawowej a interfejsem.
On mówi:
Wyobraź sobie każdą nową metodę, którą zamierzasz wdrożyć. Rozważ to dla każdego z nich: czy ta metoda zostanie zaimplementowana przez więcej niż jedną klasę w dokładnie tej formie, bez żadnych zmian? Jeśli odpowiedź brzmi „tak”, utwórz klasę podstawową. W każdej innej sytuacji utwórz interfejs.
Na przykład:
Rozważ klasy
cat
idog
, które rozszerzają klasęmammal
i mają jedną metodępet()
. Następnie dodajemy klasęalligator
, która niczego nie rozszerza i ma jedną metodęslither()
.Teraz chcemy dodać
eat()
metodę do wszystkich z nich.Jeśli implementacja
eat()
metody będzie dokładnie taka sama dlacat
,dog
ialligator
powinniśmy stworzyć klasę podstawową (powiedzmyanimal
), która implementuje tę metodę.Jeśli jednak jego implementacja
alligator
różni się w najmniejszym stopniu, powinniśmy stworzyćIEat
interfejs oraz go stworzyćmammal
ialligator
wdrożyć.
Podkreśla, że ta metoda obejmuje wszystkie przypadki, ale wydaje mi się, że to nadmierne uproszczenie.
Czy warto stosować tę zasadę?
źródło
alligator
Implementacjaeat
różni się oczywiście tym, że akceptujecat
idog
jako parametry.is a
a interfejsy toacts like
lubis
. Więc psiis a
ssak iacts like
zjadacz. To powiedziałoby nam, że ssak powinien być klasą, a zjadacz powinien być interfejsem. To zawsze był bardzo pomocny przewodnik. Sidenote: Przykłademis
może byćThe cake is eatable
lubThe book is writable
.Odpowiedzi:
Nie sądzę, że to dobra zasada. Jeśli obawiasz się ponownego wykorzystania kodu, możesz wdrożyć
PetEatingBehavior
rolę polegającą na zdefiniowaniu funkcji żywieniowych dla kotów i psów. Następnie możesz mieć możliwośćIEat
ponownego wykorzystania kodu.Obecnie widzę coraz mniej powodów do korzystania z dziedziczenia. Dużą zaletą jest łatwość użytkowania. Weź ramy GUI. Popularnym sposobem zaprojektowania interfejsu API w tym celu jest ujawnienie ogromnej klasy bazowej i udokumentowanie metod, które użytkownik może zastąpić. Możemy więc zignorować wszystkie inne skomplikowane sprawy, którymi musi zarządzać nasza aplikacja, ponieważ „domyślna” implementacja jest zapewniana przez klasę podstawową. Ale nadal możemy dostosowywać rzeczy, ponownie wdrażając poprawną metodę, gdy jest to konieczne.
Jeśli trzymasz się interfejsu API, użytkownik zwykle ma więcej pracy, aby proste przykłady działały. Ale API z interfejsami są często mniej sprzężone i łatwiejsze w utrzymaniu z prostego powodu, który
IEat
zawiera mniej informacji niżMammal
klasa podstawowa. Tak więc konsumenci będą polegać na słabszej umowie, otrzymasz więcej możliwości refaktoryzacji itp.Cytując Rich Hickey: Prosty! = Łatwy
źródło
PetEatingBehavior
wdrożenia?PetEatingBehavior
to chyba źle jeden. Nie mogę dać ci implementacji, ponieważ jest to tylko przykład zabawki. Ale mogę opisać etapy refaktoryzacji: ma konstruktor, który bierze wszystkie informacje używane przez koty / psy, aby zdefiniowaćeat
metodę (wystąpienie zębów, wystąpienie żołądka itp.). Zawiera tylko kod wspólny dla kotów i psów używanych w icheat
metodzie. Musisz po prostu utworzyćPetEatingBehavior
członka i przekazać go wywołaniom na dog.eat/cat.eat.To przyzwoita zasada, ale znam wiele miejsc, w których ją naruszam.
Aby być uczciwym, używam (abstrakcyjnych) klas podstawowych o wiele bardziej niż moi rówieśnicy. Robię to jako programowanie obronne.
Dla mnie (w wielu językach) abstrakcyjna klasa podstawowa dodaje kluczowe ograniczenie, że może istnieć tylko jedna klasa bazowa. Decydując się na użycie klasy podstawowej zamiast interfejsu, mówisz „to jest podstawowa funkcjonalność klasy (i dowolnego spadkobiercy) i niewłaściwe jest łączenie nie chcących z innymi funkcjami”. Interfejsy są odwrotne: „To jakaś cecha obiektu, niekoniecznie jego podstawowa odpowiedzialność”.
Tego rodzaju wybory projektowe tworzą niejawne przewodniki dla implementatorów o tym, w jaki sposób powinni używać twojego kodu, a przez to pisząc jego kod. Ze względu na ich większy wpływ na bazę kodu, przy podejmowaniu decyzji projektowej staram się silniej brać to pod uwagę.
źródło
Problem z uproszczeniem twojego przyjaciela polega na tym, że określenie, czy dana metoda kiedykolwiek będzie wymagać zmiany, jest wyjątkowo trudne. Lepiej jest zastosować regułę, która mówi o mentalności stojącej za superklasami i interfejsami.
Zamiast zastanawiać się, co powinieneś zrobić, patrząc na to, co uważasz za funkcjonalność, spójrz na hierarchię, którą próbujesz utworzyć.
Oto jak mój profesor nauczył różnicę:
Nadklasy powinny być,
is a
a interfejsy toacts like
lubis
. Więc psiis a
ssak iacts like
zjadacz. To powiedziałoby nam, że ssak powinien być klasą, a zjadacz powinien być interfejsem. Przykłademis
byłobyThe cake is eatable
lubThe book is writable
(co jakeatable
iwritable
interfejsy).Korzystanie z takiej metodologii jest dość proste i łatwe, i powoduje, że kodujesz do struktury i pojęć, a nie do tego, co według ciebie zrobi kod. Ułatwia to konserwację, kod jest bardziej czytelny, a projekt łatwiejszy do utworzenia.
Niezależnie od tego, co kod może faktycznie powiedzieć, jeśli użyjesz takiej metody, możesz wrócić za pięć lat i użyć tej samej metody, aby odtworzyć kroki i łatwo dowiedzieć się, jak zaprojektowano program i jak on działa.
Tylko moje dwa centy.
źródło
Na prośbę exizt rozszerzam swój komentarz na dłuższą odpowiedź.
Największym błędem, jaki popełniają ludzie, jest przekonanie, że interfejsy to po prostu puste klasy abstrakcyjne. Interfejs jest sposobem dla programisty na powiedzenie: „Nie dbam o to, co mi dajesz, o ile przestrzega tej konwencji”.
Biblioteka .NET stanowi wspaniały przykład. Na przykład, kiedy piszesz funkcję, która akceptuje
IEnumerable<T>
, mówisz: „Nie obchodzi mnie, jak przechowujesz swoje dane. Chcę tylko wiedzieć, że mogę użyćforeach
pętli.To prowadzi do bardzo elastycznego kodu. Nagle, aby się zintegrować, wystarczy grać zgodnie z zasadami istniejących interfejsów. Jeśli implementacja interfejsu jest trudna lub myląca, być może jest to wskazówka, że próbujesz wepchnąć kwadratowy kołek w okrągły otwór.
Ale potem pojawia się pytanie: „Co z ponownym użyciem kodu? Moi profesorowie CS powiedzieli mi, że dziedziczenie jest rozwiązaniem wszystkich problemów związanych z ponownym użyciem kodu i że dziedziczenie pozwala pisać raz i używać wszędzie, a leczy zapalenie opon mózgowych w procesie ratowania sierot z podnoszących się mórz i nie byłoby już łez i tak dalej, dalej itd. itd. itd. ”
Używanie dziedziczenia tylko dlatego, że podoba Ci się brzmienie słów „ponowne użycie kodu” jest dość złym pomysłem. Przewodnik po stylowaniu kodu od Google dość zwięźle:
Aby zilustrować, dlaczego dziedziczenie nie zawsze jest odpowiedzią, zamierzam użyć klasy o nazwie MySpecialFileWriter †. Osoba, która ślepo wierzy, że dziedziczenie jest rozwiązaniem wszystkich problemów, argumentowałaby, że powinieneś spróbować odziedziczyć
FileStream
, aby nie powielićFileStream
kodu. Mądrzy ludzie wiedzą, że to głupie. Powinieneś po prostu miećFileStream
obiekt w swojej klasie (jako zmienną lokalną lub zmienną składową) i korzystać z jego funkcjonalności.FileStream
Przykładem może wydawać wymyślony, ale tak nie jest. Jeśli masz dwie klasy, które obydwie implementują ten sam interfejs w dokładnie ten sam sposób, powinieneś mieć trzecią klasę, która zawiera dowolną powieloną operację. Twoim celem powinno być pisanie klas, które są samodzielnymi blokami wielokrotnego użytku, które można łączyć jak legos.Nie oznacza to, że należy unikać dziedziczenia za wszelką cenę. Jest wiele punktów do rozważenia, a większość zostanie omówiona w badaniu pytania „Kompozycja a dziedziczenie”. Nasz własny przepełnienie stosu ma kilka dobrych odpowiedzi na ten temat.
Na koniec dnia uczucia twojego współpracownika nie mają głębi ani zrozumienia niezbędnego do podjęcia świadomej decyzji. Zbadaj temat i sam go wymyśl.
† Przy ilustrowaniu dziedziczenia wszyscy używają zwierząt. To jest bezużyteczne. W ciągu 11 lat rozwoju nigdy nie napisałem klasy o nazwie
Cat
, więc nie będę jej używał jako przykładu.źródło
FileStream
)Przykłady rzadko wskazują na to, szczególnie gdy dotyczą samochodów lub zwierząt. Sposób jedzenia (lub przetwarzania żywności) zwierzęcia nie jest tak naprawdę związany z jego dziedzictwem, nie ma jednego sposobu żywienia, który obowiązywałby wszystkie zwierzęta. Jest to bardziej zależne od jego fizycznych możliwości (że tak powiem, sposobów wprowadzania, przetwarzania i produkcji). Ssaki mogą być na przykład mięsożercami, roślinożercami lub wszystkożercami, podczas gdy ptaki, ssaki i ryby mogą jeść owady. To zachowanie można uchwycić za pomocą określonych wzorów.
Lepiej jest w tym przypadku wyodrębnić zachowanie, takie jak to:
Lub zaimplementuj wzór odwiedzin, w którym
FoodProcessor
próbuje się karmić kompatybilne zwierzęta (ICarnivore : IFoodEater
,MeatProcessingVisitor
). Myśl o żywych zwierzętach może przeszkadzać w myśleniu o zerwaniu ich przewodu pokarmowego i zastąpieniu go ogólnym, ale naprawdę wyświadczasz im przysługę.To sprawi, że twoje zwierzę będzie klasą podstawową, a jeszcze lepiej, której już nigdy nie będziesz musiał zmieniać, dodając nowy rodzaj jedzenia i odpowiedni procesor, dzięki czemu możesz skupić się na rzeczach, które sprawiają, że ta konkretna klasa zwierząt jest pracować nad tak wyjątkowymi.
Tak więc, klasy podstawowe mają swoje miejsce, obowiązuje zasada kciuka (często nie zawsze, ponieważ jest to zasada kciuka), ale szukaj dalej zachowania, które możesz izolować.
źródło
IConsumer
interfejs. Mięsożercy mogą spożywać mięso, zwierzęta roślinożerne - rośliny, a rośliny - światło słoneczne i wodę.Interfejs to tak naprawdę umowa. Brak funkcji. Wdrożenie należy do implementatora. Nie ma wyboru, ponieważ należy wdrożyć umowę.
Klasy abstrakcyjne dają implementatorom wybór. Można wybrać metodę, którą każdy musi wdrożyć, zapewnić metodę z podstawową funkcjonalnością, którą można zastąpić, lub zapewnić podstawową funkcjonalność, z której mogą korzystać wszyscy spadkobiercy.
Na przykład:
Powiedziałbym, że twoją praktyczną zasadą mogłaby również zająć się klasa podstawowa. W szczególności, ponieważ wiele ssaków prawdopodobnie „je” tak samo, wówczas użycie klasy bazowej może być lepsze niż interfejs, ponieważ można zaimplementować metodę wirtualnego jedzenia, z której może skorzystać większość spadkobierców, a wówczas wyjątkowe przypadki mogą zostać pominięte.
Teraz, jeśli określenie „jedzenia” różni się znacznie w zależności od zwierzęcia, być może i interfejs byłby lepszy. Czasami jest to szara strefa i zależy od indywidualnej sytuacji.
źródło
Nie.
Interfejsy dotyczą sposobu implementacji metod. Jeśli opierasz swoją decyzję o tym, kiedy użyć interfejsu, na tym, w jaki sposób metody zostaną zaimplementowane, robisz coś złego.
Dla mnie różnica między interfejsem a klasą podstawową zależy od tego, jakie są wymagania. Interfejs tworzy się, gdy jakiś fragment kodu wymaga klasy z określonym interfejsem API i domyślnym zachowaniem wynikającym z tego interfejsu API. Nie można utworzyć interfejsu bez kodu, który faktycznie go wywoła.
Z drugiej strony, klasy podstawowe są zwykle rozumiane jako sposób na ponowne użycie kodu, więc rzadko zawracasz sobie głowę kodem, który wywoła tę klasę podstawową i bierzesz pod uwagę jedynie hierarchię obiektów, do których należy ta klasa podstawowa.
źródło
eat()
po co wdrażać ponownie u każdego zwierzęcia? Możemy tomammal
wdrożyć, prawda? Rozumiem jednak twoje zdanie na temat wymagań.To nie jest dobra zasada, ponieważ jeśli chcesz różnych implementacji, zawsze możesz mieć abstrakcyjną klasę bazową z abstrakcyjną metodą. Każdy spadkobierca ma zapewnioną tę metodę, ale każdy określa własną implementację. Technicznie rzecz biorąc, mogłaby istnieć klasa abstrakcyjna bez zestawu metod abstrakcyjnych i byłaby w zasadzie taka sama jak interfejs.
Klasy podstawowe są przydatne, gdy chcesz rzeczywistej implementacji pewnego stanu lub zachowania, które może być użyte w kilku klasach. Jeśli znajdziesz się w kilku klasach, które mają identyczne pola i metody z identycznymi implementacjami, prawdopodobnie możesz to zmienić na klasę podstawową. Musisz po prostu zachować ostrożność podczas dziedziczenia. Każde istniejące pole lub metoda w klasie bazowej musi mieć sens w spadkobiercy, szczególnie gdy jest ona rzutowana jako klasa bazowa.
Interfejsy są przydatne, gdy trzeba wchodzić w interakcje z zestawem klas w ten sam sposób, ale nie można przewidzieć ani nie przejmować się ich faktyczną implementacją. Weźmy na przykład coś w rodzaju interfejsu kolekcji. Pan zna tylko wszystkie niezliczone sposoby, w jakie ktoś może zdecydować się na wdrożenie zbioru przedmiotów. Połączona lista? Lista tablic? Stos? Kolejka? Ta metoda SearchAndReplace nie ma znaczenia, wystarczy przekazać coś, co może wywołać Dodaj, Usuń i GetEnumerator.
źródło
W pewnym sensie osobiście obserwuję odpowiedzi na to pytanie, aby zobaczyć, co mają do powiedzenia inni ludzie, ale powiem trzy rzeczy, o których jestem skłonny pomyśleć:
Ludzie pokładają zbyt wiele wiary w interfejsy, a zbyt mało wiary w klasy. Interfejsy są w gruncie rzeczy bardzo rozwodnionymi klasami podstawowymi i zdarzają się sytuacje, w których użycie czegoś lekkiego może prowadzić do takich rzeczy, jak nadmierny kod płyty bazowej.
Nie ma nic złego w przesłonięciu metody, wywołaniu
super.method()
jej i pozwoleniu reszcie jego ciała na robienie rzeczy, które nie kolidują z klasą podstawową. Nie narusza to zasady substytucji Liskowa .Z reguły bycie tak sztywnym i dogmatycznym w inżynierii oprogramowania jest złym pomysłem, nawet jeśli coś ogólnie jest najlepszą praktyką.
Więc wziąłbym to, co powiedziano, z dodatkiem soli.
źródło
People put way too much faith into interfaces, and too little faith into classes.
Zabawny. Wydaje mi się, że jest odwrotnie.Chcę przyjąć do tego językowe i semantyczne podejście. Interfejs nie istnieje
DRY
. Chociaż może być używany jako mechanizm centralizacji, a także posiada abstrakcyjną klasę, która go implementuje, nie jest przeznaczony do DRY.Interfejs to umowa.
IAnimal
Interfejs jest umową typu „wszystko albo nic” między aligatorem, kotem, psem i pojęciem zwierzęcia. Mówi w gruncie rzeczy „jeśli chcesz być zwierzęciem, albo musisz mieć wszystkie te cechy i cechy, albo nie jesteś już zwierzęciem”.Dziedziczenie po drugiej stronie jest mechanizmem ponownego wykorzystania. W tym przypadku uważam, że dobre rozumowanie semantyczne może pomóc ci zdecydować, która metoda powinna być dokąd. Na przykład, podczas gdy wszystkie zwierzęta mogą spać i śpią tak samo, nie użyję do tego klasy podstawowej. Najpierw umieściłem
Sleep()
metodę wIAnimal
interfejsie jako nacisk (semantyczny) na to, co wymaga bycia zwierzęciem, a następnie używam podstawowej abstrakcyjnej klasy dla kota, psa i aligatora jako techniki programowania, aby się nie powtarzać.źródło
Nie jestem pewien, czy jest to najlepsza zasada. Możesz umieścić
abstract
metodę w klasie bazowej i pozwolić każdej klasie ją zaimplementować. Ponieważ każdymammal
musi jeść, miałoby to sens. Następnie każdymammal
może użyć swojegoeat
metody. Zwykle używam interfejsów tylko do komunikacji między obiektami lub jako znacznik do oznaczenia klasy jako czegoś. Myślę, że nadużywanie interfejsów powoduje niechlujny kod.Zgadzam się również z @Panzercrisis, że ogólna zasada powinna być ogólną wytyczną. Nie coś, co powinno być napisane w kamieniu. Niewątpliwie będą chwile, w których to po prostu nie zadziała.
źródło
Interfejsy są typami. Nie zapewniają wdrożenia. Po prostu definiują umowę na obsługę tego typu. Ta umowa musi być spełniona przez każdą klasę implementującą interfejs.
Zastosowanie interfejsów i klas abstrakcyjnych / podstawowych powinno być określone przez wymagania projektowe.
Pojedyncza klasa nie wymaga interfejsu.
Jeśli dwie funkcje są wymienne w ramach funkcji, powinny implementować wspólny interfejs. Każda funkcjonalność, która jest implementowana identycznie, mogłaby zostać przekształcona w abstrakcyjną klasę implementującą interfejs. Interfejs może być w tym momencie uznany za zbędny, jeśli nie ma funkcji wyróżniającej. Ale jaka jest różnica między tymi dwiema klasami?
Używanie klas abstrakcyjnych jako typów jest na ogół niechlujne, ponieważ dodaje się więcej funkcji i rozwija się hierarchia.
Preferuj kompozycję zamiast dziedziczenia - wszystko to odchodzi.
źródło