Mój współpracownik i ja mamy różne opinie na temat relacji między klasami podstawowymi a interfejsami. Jestem przekonany, że klasa nie powinna implementować interfejsu, chyba że z tej klasy można korzystać, gdy wymagana jest implementacja interfejsu. Innymi słowy, lubię widzieć taki kod:
interface IFooWorker { void Work(); }
abstract class BaseWorker {
... base class behaviors ...
public abstract void Work() { }
protected string CleanData(string data) { ... }
}
class DbWorker : BaseWorker, IFooWorker {
public void Work() {
Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
}
}
Interfejs DbWorker dostaje interfejs IFooWorker, ponieważ jest on możliwą do natychmiastowego wdrożenia implementacją interfejsu. Całkowicie wypełnia umowę. Mój współpracownik woli prawie identyczne:
interface IFooWorker { void Work(); }
abstract class BaseWorker : IFooWorker {
... base class behaviors ...
public abstract void Work() { }
protected string CleanData(string data) { ... }
}
class DbWorker : BaseWorker {
public void Work() {
Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
}
}
Tam, gdzie klasa podstawowa otrzymuje interfejs, i dzięki temu wszyscy spadkobiercy klasy podstawowej również mają ten interfejs. To mnie wkurza, ale nie mogę wymyślić konkretnych powodów, dla których poza „klasą podstawową nie może stać samodzielnie jako implementacja interfejsu”.
Jakie są zalety i wady jego metody w porównaniu do mojej i dlaczego należy stosować jedną nad drugą?
źródło
Work
jest kwestią dziedziczenia diamentów (w takim przypadku oboje wypełniają umowę dotyczącą tejWork
metody). W Javie musisz zaimplementować metody interfejsu, więcWork
w ten sposób można uniknąć problemu, z którego programu powinien korzystać program. Języki takie jak C ++ nie są jednak dla ciebie jednoznaczne.Odpowiedzi:
BaseWorker
spełnia ten wymóg. To, że nie możesz bezpośrednio utworzyć instancjiBaseWorker
obiektu, nie oznacza, że nie możesz miećBaseWorker
wskaźnika spełniającego umowę. Rzeczywiście, taki jest właśnie cel klas abstrakcyjnych.Poza tym trudno jest stwierdzić na podstawie opublikowanego uproszczonego przykładu, ale część problemu może polegać na tym, że interfejs i klasa abstrakcyjna są zbędne. O ile nie masz innych implementujących klas
IFooWorker
, które nie pochodzą odBaseWorker
ciebie, w ogóle nie potrzebujesz interfejsu. Interfejsy to tylko abstrakcyjne klasy z pewnymi dodatkowymi ograniczeniami, które ułatwiają wielokrotne dziedziczenie.Ponownie trudno jest stwierdzić na podstawie uproszczonego przykładu, użycie chronionej metody, wyraźnie odnoszącej się do bazy z klasy pochodnej, oraz brak jednoznacznego miejsca do zadeklarowania implementacji interfejsu są znakami ostrzegawczymi, że niewłaściwie używasz dziedziczenia zamiast kompozycja. Bez tego dziedzictwa całe twoje pytanie staje się dyskusyjne.
źródło
BaseWorker
nie jest możliwe do natychmiastowego wygenerowania, nie oznacza, że danaBaseWorker myWorker
nie implementuje sięIFooWorker
.Muszę się zgodzić z twoim współpracownikiem.
W obu podanych przykładach BaseWorker definiuje metodę abstrakcyjną Work (), co oznacza, że wszystkie podklasy są w stanie spełnić kontrakt IFooWorker. W tym przypadku myślę, że BaseWorker powinien zaimplementować interfejs i ta implementacja zostanie odziedziczona przez jego podklasy. Pozwoli to uniknąć konieczności jawnego wskazywania, że każda podklasa jest rzeczywiście IFooWorker (zasada DRY).
Jeśli nie zdefiniowałeś Work () jako metody BaseWorker lub jeśli IFooWorker miał inne metody, których podklasy BaseWorker nie chciałyby i nie potrzebowały, wtedy (oczywiście) musiałbyś wskazać, które podklasy faktycznie implementują IFooWorker.
źródło
IFoo
aBaseFoo
to oznacza, że alboIFoo
nie jest odpowiednio drobnoziarnisty interfejs, albo żeBaseFoo
jest za pomocą dziedziczenia, gdzie kompozycja może być lepszym wyborem.MyDaoFoo
lubSqlFoo
lubHibernateFoo
lub cokolwiek, aby wskazać, że jest to tylko jeden z możliwych drzewo klas wykonawczychIFoo
? Jeszcze bardziej pachnie, jeśli klasa podstawowa jest połączona z określoną biblioteką lub platformą i nie ma o tym wzmianki w nazwie ...Generalnie zgadzam się z twoim współpracownikiem.
Weźmy twój model: interfejs jest implementowany tylko przez klasy potomne, mimo że klasa podstawowa wymusza także ujawnienie metod IFooWorker. Po pierwsze, jest zbędny; bez względu na to, czy klasa potomna implementuje interfejs, czy nie, muszą przesłonić ujawnione metody BaseWorker, podobnie jak każda implementacja IFooWorker musi ujawniać funkcjonalność, niezależnie od tego, czy otrzyma jakąkolwiek pomoc od BaseWorker, czy nie.
To dodatkowo sprawia, że twoja hierarchia jest niejednoznaczna; „Wszyscy pracownicy IFoo są pracownikami Baseoo” niekoniecznie są prawdziwymi stwierdzeniami, a także „Wszyscy pracownicy Baseoo są pracownikami IFoo”. Tak więc, jeśli chcesz zdefiniować zmienną instancji, parametr lub typ zwracany, która może być dowolną implementacją IFooWorker lub BaseWorker (korzystając ze wspólnej ujawnionej funkcjonalności, która jest jednym z powodów dziedziczenia w pierwszej kolejności), żadna z gwarantuje się, że obejmują one wszystko; niektórych BaseWorkers nie można przypisać do zmiennej typu IFooWorker i odwrotnie.
Model twojego współpracownika jest znacznie łatwiejszy w użyciu i wymianie. „Wszyscy pracownicy BaseWorkers są IFooWorkers” to teraz prawdziwe stwierdzenie; możesz przekazać dowolną instancję BaseWorker dowolnej zależności IFooWorker, bez żadnych problemów. Przeciwne stwierdzenie „Wszyscy pracownicy IFoo są pracownikami podstawowymi” nie jest prawdziwy; która pozwala zastąpić BaseWorker BetterBaseWorker, a Twój kod konsumencki (zależny od IFooWorker) nie będzie musiał odróżniać.
źródło
Muszę dodać ostrzeżenie do tych odpowiedzi. Sprzężenie klasy bazowej z interfejsem tworzy siłę w strukturze tej klasy. W twoim podstawowym przykładzie nie jest żadnym problemem, że oba powinny być ze sobą połączone, ale może to nie być prawdą we wszystkich przypadkach.
Weź klasy frameworku Java:
Fakt, że umowa kolejki jest implementowana przez LinkedList, nie pchnął problemu do AbstractList .
Jaka jest różnica między tymi dwoma przypadkami? Celem BaseWorker było zawsze (zgodnie z nazwą i interfejsem) wdrożenie operacji w IWorker . Cel AbstractList i kolejki są rozbieżne, ale potomek pierwszego może nadal realizować drugi kontrakt.
źródło
Zadałbym pytanie, co się stanie po zmianie
IFooWorker
, na przykład dodanie nowej metody?Jeśli
BaseWorker
implementuje interfejs, domyślnie będzie musiał zadeklarować nową metodę, nawet jeśli jest abstrakcyjna. Z drugiej strony, jeśli nie zaimplementuje interfejsu, otrzymasz błędy kompilacji tylko w klasach pochodnych.Z tego powodu sprawiłbym, że klasa podstawowa zaimplementowała interfejs , ponieważ mogłabym być w stanie zaimplementować całą funkcjonalność nowej metody w klasie podstawowej bez dotykania klas pochodnych.
źródło
Najpierw zastanów się, czym jest abstrakcyjna klasa bazowa i czym jest interfejs. Zastanów się, kiedy użyjesz jednego lub drugiego, a kiedy nie.
Ludzie często myślą, że oba są bardzo podobnymi pojęciami, w rzeczywistości jest to częste pytanie podczas rozmowy kwalifikacyjnej (różnica między nimi jest ??)
Tak więc abstrakcyjna klasa bazowa daje coś, czego interfejsy nie mogą, co jest domyślną implementacją metod. (Są inne rzeczy, w C # możesz mieć metody statyczne na klasie abstrakcyjnej, a nie na interfejsie na przykład).
Jako przykład, jednym z powszechnych zastosowań jest IDisposable. Klasa abstrakcyjna implementuje metodę Dispose IDisposable, co oznacza, że każda pochodna wersja klasy podstawowej będzie automatycznie dostępna. Następnie możesz grać z kilkoma opcjami. Utwórz Dispose streszczenie, zmuszając klasy pochodne do jego wdrożenia. Zapewnij wirtualną domyślną implementację, więc nie będą, ani nie będą wirtualne ani abstrakcyjne, i będą wywoływać wirtualne metody zwane na przykład takimi jak BeforeDispose, AfterDispose, OnDispose lub Disposing.
Tak więc za każdym razem, gdy wszystkie klasy pochodne muszą obsługiwać interfejs, przechodzi on do klasy podstawowej. Jeśli tylko jeden lub niektórzy potrzebują tego interfejsu, przejdzie do klasy pochodnej.
To wszystko w rzeczywistości jest rażące w stosunku do uproszczenia. Inną alternatywą jest posiadanie klas pochodnych, które nawet nie implementują interfejsów, ale zapewniają je poprzez wzorzec adaptera. Przykładem tego, który ostatnio widziałem, jest IObjectContextAdapter.
źródło
Jeszcze wiele lat dzieli mnie od pełnego zrozumienia różnicy między klasą abstrakcyjną a interfejsem. Za każdym razem, gdy myślę, że rozumiem podstawowe pojęcia, szukam wymiany stosów i mam dwa kroki do tyłu. Ale kilka przemyśleń na ten temat i pytanie PO:
Pierwszy:
Istnieją dwa popularne wyjaśnienia interfejsu:
Interfejs to lista metod i właściwości, które może zaimplementować dowolna klasa, a poprzez implementację interfejsu klasa gwarantuje, że te metody (i ich podpisy) oraz te właściwości (i ich typy) będą dostępne, gdy będą „współpracować” z tą klasą lub obiekt tej klasy. Interfejs to umowa.
Interfejsy to abstrakcyjne klasy, które nic nie mogą / nie mogą zrobić. Są przydatne, ponieważ można zaimplementować więcej niż jedną, w przeciwieństwie do tych średnich klas nadrzędnych. Na przykład mógłbym być obiektem klasy BananaBread i dziedziczyć z BaseBread, ale to nie znaczy, że nie mogę również implementować zarówno interfejsów IWithNuts, jak i ITastesYummy. Mógłbym nawet wdrożyć interfejs IDoesTheDishes, bo nie jestem tylko chlebem, wiesz?
Istnieją dwa popularne wyjaśnienia klasy abstrakcyjnej:
Abstrakcyjna klasa, yknow The coś nie coś, co można zrobić. To tak naprawdę esencja, wcale nie jest prawdziwa. Poczekaj, to pomoże. Łódź to klasa abstrakcyjna, ale seksowny jacht Playboy byłby podklasą BaseBoat.
Czytam książkę o klasach abstrakcyjnych i być może powinieneś ją przeczytać, ponieważ prawdopodobnie jej nie rozumiesz i zrobi to źle, jeśli nie przeczytasz tej książki.
Nawiasem mówiąc, książka Citers zawsze wydaje się imponująca, nawet jeśli nadal jestem zdezorientowany.
Druga:
Na SO ktoś zadał prostszą wersję tego pytania, klasyczne: „po co używać interfejsów? Jaka jest różnica? Czego mi brakuje?” W jednej odpowiedzi wykorzystano pilota sił powietrznych jako prosty przykład. Nie całkiem wylądował, ale wywołał kilka świetnych komentarzy, z których jeden wspomniał o interfejsie IFlyable z metodami takimi jak takeOff, pilotEject itp. I to naprawdę kliknęło mnie jako prawdziwy przykład, dlaczego interfejsy są nie tylko przydatne, ale kluczowe. Interfejs sprawia, że obiekt / klasa jest intuicyjna lub przynajmniej daje wrażenie, że jest. Interfejs nie działa na korzyść obiektu ani danych, ale na coś, co wymaga interakcji z tym obiektem. Klasyczny Fruit-> Apple-> Fuji or Shape-> Triangle-> Równoległe przykłady dziedziczenia są doskonałym modelem do taksonomicznego zrozumienia danego obiektu w oparciu o jego potomków. Informuje konsumenta i przetwórców o jego ogólnych cechach, zachowaniach, bez względu na to, czy obiekt jest grupą rzeczy, podłącza system do systemu, jeśli umieścisz go w niewłaściwym miejscu, lub opisuje dane sensoryczne, złącze do określonego magazynu danych lub dane finansowe potrzebne do sporządzenia listy płac.
Określony obiekt Plane może, ale nie musi mieć metody awaryjnego lądowania, ale będę wkurzony, jeśli założę jego awaryjny ląd i jak każdy inny obiekt latający, aby obudzić się w DumbPlane i dowiedzieć się, że programiści poszli z QuickLand ponieważ myśleli, że to wszystko tak samo. Tak jak byłbym sfrustrowany, gdyby każdy producent śrub miał swoją własną interpretację righty tighty lub jeśli mój telewizor nie miał przycisku zwiększania głośności powyżej przycisku zmniejszania głośności.
Klasy abstrakcyjne to model, który określa, co dowolny potomek musi mieć, aby kwalifikować się jako ta klasa. Jeśli nie masz wszystkich cech kaczki, nie ma znaczenia, że masz zaimplementowany interfejs IQuack, twój po prostu dziwny pingwin. Interfejsy to rzeczy, które mają sens, nawet jeśli nie możesz być pewien niczego innego. Jeff Goldblum i Starbuck byli w stanie latać kosmicznymi statkami kosmicznymi, ponieważ interfejs był niezawodnie podobny.
Trzeci:
Zgadzam się z twoim współpracownikiem, ponieważ czasami musisz egzekwować pewne metody na samym początku. Jeśli tworzysz ORM rekordu aktywnego, wymaga on metody zapisu. To nie zależy od podklasy, którą można utworzyć. A jeśli interfejs ICRUD jest na tyle przenośny, że nie może być sprzężony wyłącznie z jedną klasą abstrakcyjną, może zostać zaimplementowany przez inne klasy, aby uczynić je niezawodnymi i intuicyjnymi dla każdego, kto już zna dowolną z klas potomnych tej klasy abstrakcyjnej.
Z drugiej strony wcześniej był świetny przykład, kiedy nie przeskakiwać do wiązania interfejsu z klasą abstrakcyjną, ponieważ nie wszystkie typy list będą (lub powinny) implementować interfejs kolejki. Powiedziałeś, że ten scenariusz zdarza się w połowie czasu, co oznacza, że ty i twój współpracownik jesteście w błędzie w połowie czasu, a zatem najlepszą rzeczą do zrobienia jest kłótnia, debata, rozważenie, a jeśli okażą się słuszne, uznanie i zaakceptowanie połączenia . Ale nie zostań programistą, który postępuje zgodnie z filozofią, nawet jeśli nie jest ona najlepsza dla danego zadania.
źródło
Jest jeszcze jeden aspekt, który
DbWorker
należy wdrożyćIFooWorker
, jak sugerujesz.W przypadku stylu twojego współpracownika, jeśli z jakiegoś powodu nastąpi późniejsza refaktoryzacja i
DbWorker
uznaje się, że nie rozszerzaBaseWorker
klasy abstrakcyjnej ,DbWorker
traciIFooWorker
interfejs. Może to, ale niekoniecznie, mieć wpływ na klientów korzystających z niego, jeśli oczekująIFooWorker
interfejsu.źródło
Na początek lubię inaczej definiować interfejsy i klasy abstrakcyjne:
W twoim przypadku wszyscy pracownicy wdrażają,
work()
ponieważ tego wymaga od nich umowa. Dlatego klasa podstawowaBaseworker
powinna implementować tę metodę jawnie lub jako funkcję wirtualną. Sugerowałbym, abyś umieścił tę metodę w swojej klasie podstawowej ze względu na prosty fakt, że wszyscy pracownicy muszą to zrobićwork()
. Dlatego logiczne jest, że twoja klasa podstawowa (nawet jeśli nie możesz utworzyć wskaźnika tego typu) zawiera ją jako funkcję.Teraz
DbWorker
spełniaIFooWorker
interfejs, ale jeśli istnieje konkretny sposób, w jaki działa ta klasawork()
, to naprawdę musi przeciążać odziedziczoną definicjęBaseWorker
. Powodem, dla którego nie powinien implementować interfejsuIFooWorker
bezpośrednio, jest to, że nie jest to coś specjalnegoDbWorker
. Gdybyś to robił za każdym razem, gdy wdrażałeś podobne klasyDbWorker
, naruszyłbyś DRY .Jeśli dwie klasy zaimplementują tę samą funkcję na różne sposoby, możesz zacząć szukać największej wspólnej nadklasy. Przez większość czasu można go znaleźć. Jeśli nie, szukaj dalej lub poddaj się i zaakceptuj, że nie mają one wystarczającej liczby wspólnych elementów, aby stworzyć klasę podstawową.
źródło
Wow, wszystkie te odpowiedzi i żadna z nich nie wskazuje, że interfejs w tym przykładzie nie ma żadnego celu. Nie używasz dziedziczenia i interfejsu dla tej samej metody, jest to bezcelowe. Używasz jednego lub drugiego. Na tym forum jest mnóstwo pytań z odpowiedziami wyjaśniającymi zalety każdego z nich i senariów, które wymagają jednego lub drugiego.
źródło