Interfejsy w klasie abstrakcyjnej

30

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ą?

Bryan Boettcher
źródło
Twoja sugestia bardzo przypomina dziedziczenie diamentów , co może powodować wiele zamieszania w dalszej części.
Spoike
@Spoike: Jak to zobaczyć?
Niing
@Niing link, który podałem w komentarzu, ma prosty schemat klasy i proste wyjaśnienie tego, co to jest. nazywa się to problemem „diamentowym”, ponieważ struktura dziedziczenia jest zasadniczo rysowana jak kształt rombu (tzn. ♦ ️).
Spoike
@Spoike: dlaczego to pytanie spowoduje dziedziczenie diamentów?
Niing
1
@Niing: Dziedziczenie BaseWorker i IFooWorker przy użyciu tej samej metody Workjest kwestią dziedziczenia diamentów (w takim przypadku oboje wypełniają umowę dotyczącą tej Workmetody). W Javie musisz zaimplementować metody interfejsu, więc Workw 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.
Spoike

Odpowiedzi:

24

Jestem przekonany, że klasa nie powinna implementować interfejsu, chyba że z tej klasy można korzystać, gdy wymagana jest implementacja interfejsu.

BaseWorkerspełnia ten wymóg. To, że nie możesz bezpośrednio utworzyć instancji BaseWorkerobiektu, 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ą od BaseWorkerciebie, 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.

Karl Bielefeldt
źródło
1
+1 za wskazanie, że tylko dlatego, że BaseWorkernie jest możliwe do natychmiastowego wygenerowania, nie oznacza, że ​​dana BaseWorker myWorkernie implementuje się IFooWorker.
Avner Shahar-Kashtan
2
Nie zgadzam się, że interfejsy to tylko abstrakcyjne klasy. Klasy abstrakcyjne zwykle definiują niektóre szczegóły implementacji (w przeciwnym razie zostałby użyty interfejs). Jeśli te szczegóły ci się nie podobają, musisz teraz zmienić każde użycie klasy podstawowej na coś innego. Interfejsy są płytkie; definiują relację „plug-and-socket” między zależnościami i zależnościami. W związku z tym ścisłe połączenie z dowolnym szczegółem wdrożenia nie stanowi już problemu. Jeśli klasa dziedzicząca interfejs nie odpowiada Ci, usuń go i włóż coś zupełnie innego; twój kod może mniej obchodzić.
KeithS
5
Oba mają zalety, ale zazwyczaj interfejs powinien być zawsze używany do zależności, niezależnie od tego, czy istnieje abstrakcyjna implementacja definiująca klasę podstawową. Połączenie interfejsu i klasy abstrakcyjnej daje to, co najlepsze z obu światów; interfejs utrzymuje relację wtyczki i gniazda płytkiej powierzchni, podczas gdy klasa abstrakcyjna zapewnia użyteczny wspólny kod. Możesz rozebrać i wymienić dowolny poziom tego pod interfejsem do woli.
KeithS
Przez „wskaźnik” rozumiesz instancję obiektu (do której wskazywałby wskaźnik)? Interfejsy nie są klasami, są typami i mogą działać jako niekompletny zamiennik wielokrotnego dziedziczenia, a nie zamiennik - to znaczy „obejmują zachowanie z wielu źródeł w klasie”.
samis
46

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.

Matthew Flynn
źródło
11
+1 opublikowałoby to samo. Przykro mi, ale myślę, że twój współpracownik ma również rację w tym przypadku, jeśli coś gwarantuje wykonanie umowy, powinna ona odziedziczyć tę umowę, niezależnie od tego, czy gwarancję spełnia interfejs, klasa abstrakcyjna, czy konkretna klasa. Mówiąc wprost: poręczyciel powinien odziedziczyć umowę, którą gwarantuje.
Jimmy Hoffa
2
Generalnie się zgadzam, ale chciałbym zauważyć, że słowa takie jak „baza”, „standard”, „domyślny”, „ogólny” itp. Są zapachami kodu w nazwie klasy. Jeśli abstrakcyjna klasa podstawowa ma prawie taką samą nazwę jak interfejs, ale zawiera jedno z tych słów łasicy, jest to często znak nieprawidłowej abstrakcji; interfejsy definiują, co coś robi , dziedziczenie typu określa, co to jest . Jeśli masz IFooa BaseFooto oznacza, że albo IFoonie jest odpowiednio drobnoziarnisty interfejs, albo że BaseFoojest za pomocą dziedziczenia, gdzie kompozycja może być lepszym wyborem.
Aaronaught
@Aaronaught Dobra uwaga, chociaż może się zdarzyć, że naprawdę istnieje coś, co wszystkie lub większość implementacji IFoo musi odziedziczyć (takie jak uchwyt usługi lub implementacja DAO).
Matthew Flynn
2
Wtedy nie powinien być nazywany klasa bazowa MyDaoFoolub SqlFoolub HibernateFoolub cokolwiek, aby wskazać, że jest to tylko jeden z możliwych drzewo klas wykonawczych IFoo? Jeszcze bardziej pachnie, jeśli klasa podstawowa jest połączona z określoną biblioteką lub platformą i nie ma o tym wzmianki w nazwie ...
Aaronaught
@Aaronaught - Tak. Zgadzam się całkowicie.
Matthew Flynn
11

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ć.

KeithS
źródło
Podoba mi się brzmienie tej odpowiedzi, ale nie do końca podążam za ostatnią częścią. Sugerujesz, że BetterBaseWorker byłby niezależną klasą abstrakcyjną, która implementuje IFooWorker, aby moja hipotetyczna wtyczka mogła po prostu powiedzieć „och chłopcze! Better Base Worker jest już dostępny!” i zmienić dowolne odniesienia BaseWorker na BetterBaseWorker i nazwać to dniem (może pobawić się nowymi, fajnymi wartościami, które pozwolą mi usunąć ogromne fragmenty mojego nadmiarowego kodu itp.)?
Anthony
Mniej więcej tak. Dużą zaletą jest to, że zmniejszysz liczbę odwołań do BaseWorker, które będą musiały zmienić się na BetterBaseWorkers; większość twojego kodu nie będzie odwoływać się bezpośrednio do konkretnej klasy, zamiast tego za pomocą IFooWorker, więc kiedy zmienisz to, co jest przypisane do tych zależności IFooWorker (właściwości klas, parametry konstruktorów lub metod), kod za pomocą IFooWorker nie powinien widzieć żadnej różnicy w użyciu.
KeithS
6

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:

abstract class AbstractList
class LinkedList extends AbstractList implements Queue

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.

Mihai Danila
źródło
Dzieje się tak przez połowę czasu. Zawsze woli implementować interfejs na klasie bazowej, a ja zawsze wolę implementować go na ostatecznym betonie. Przedstawiony przez ciebie scenariusz zdarza się często i jest to część powodów, dla których przeszkadzają mi interfejsy w bazie.
Bryan Boettcher,
1
Racja, insta, i uważam użycie interfejsów w ten sposób za aspekt nadużycia dziedziczenia, które widzimy w wielu środowiskach programistycznych. Jak powiedział GoF w swoim wzorcu projektowym, wolę kompozycję niż dziedziczenie , a utrzymanie interfejsu poza klasą podstawową jest jednym ze sposobów promowania tej samej zasady.
Mihai Danila,
1
Dostaję ostrzeżenie, ale zauważysz, że klasa abstrakcyjna OP zawierała metody abstrakcyjne pasujące do interfejsu: BaseWorker jest domyślnie IFooWorker. Wyjaśnienie tego faktu czyni ten fakt bardziej użytecznym. Istnieje jednak wiele metod zawartych w kolejce, które nie są uwzględnione w AbstractList, i jako takie, AbstractList nie jest kolejką, nawet jeśli mogłyby to być jej elementy potomne. AbstractList i Queue są ortogonalne.
Matthew Flynn
Dziękuję za ten wgląd; Zgadzam się, że przypadek PO mieści się w tej kategorii, ale czułem, że doszło do szerszej dyskusji w poszukiwaniu głębszej reguły i chciałem podzielić się spostrzeżeniami na temat tendencji, o której czytałem (a nawet miałem siebie przez krótki czas) nadużywania dziedziczenia, być może dlatego, że jest to jedno z narzędzi w przyborniku OOP.
Mihai Danila
Dang, minęło sześć lat. :)
Mihai Danila
2

Zadałbym pytanie, co się stanie po zmianie IFooWorker, na przykład dodanie nowej metody?

Jeśli BaseWorkerimplementuje 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.

Scott Whitlock
źródło
1

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.

Ian
źródło
1

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:

  1. 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.

  2. 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:

  1. 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.

  2. 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.

Anthony
źródło
0

Jest jeszcze jeden aspekt, który DbWorkernależy wdrożyć IFooWorker, jak sugerujesz.

W przypadku stylu twojego współpracownika, jeśli z jakiegoś powodu nastąpi późniejsza refaktoryzacja i DbWorkeruznaje się, że nie rozszerza BaseWorkerklasy abstrakcyjnej , DbWorkertraci IFooWorkerinterfejs. Może to, ale niekoniecznie, mieć wpływ na klientów korzystających z niego, jeśli oczekują IFooWorkerinterfejsu.

Frederik Krautwald
źródło
-1

Na początek lubię inaczej definiować interfejsy i klasy abstrakcyjne:

Interface: a contract that each class must fulfill if they are to implement that interface
Abstract class: a general class (i.e vehicle is an abstract class, while porche911 is not)

W twoim przypadku wszyscy pracownicy wdrażają, work()ponieważ tego wymaga od nich umowa. Dlatego klasa podstawowa Baseworkerpowinna 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 DbWorkerspełnia IFooWorkerinterfejs, ale jeśli istnieje konkretny sposób, w jaki działa ta klasa work(), to naprawdę musi przeciążać odziedziczoną definicję BaseWorker. Powodem, dla którego nie powinien implementować interfejsu IFooWorkerbezpośrednio, jest to, że nie jest to coś specjalnego DbWorker. Gdybyś to robił za każdym razem, gdy wdrażałeś podobne klasy DbWorker, 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ą.

Arnab Datta
źródło
-3

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.

Martin Maat
źródło
3
To kłamstwo patentowe. Interfejs to umowa, na podstawie której oczekuje się, że system będzie zachowywał się bez względu na implementację. Dziedziczona klasa podstawowa jest jedną z wielu możliwych implementacji tego interfejsu. System o wystarczająco dużej skali będzie miał wiele klas podstawowych, które spełniają różne interfejsy (zwykle zamknięte za pomocą generycznych), co dokładnie zrobiło to ustawienie i dlaczego warto je było podzielić.
Bryan Boettcher
To naprawdę zły przykład z perspektywy OOP. Jak mówisz „klasa podstawowa jest tylko jedną implementacją” (powinna być lub nie ma sensu interfejs), mówisz, że będą klasy inne niż robocze implementujące interfejs IFooWorker. Wtedy będziesz miał klasę podstawową, która jest wyspecjalizowanym współpracownikiem (powinniśmy założyć, że mogą być również pracownicy barworkerscy), a będziesz miał innych współpracowników, którzy nie są nawet podstawowymi pracownikami! To jest zacofane. Na więcej niż jeden sposób.
Martin Maat
1
Być może utknąłeś na słowie „Worker” w interfejsie. Co z „źródłem danych”? Jeśli mamy IDatasource <T>, możemy w prosty sposób mieć SqlDatasource <T>, RestDatasource <T>, MongoDatasource <T>. Firma decyduje, które zamknięcia interfejsu generycznego przechodzą do końcowych implementacji abstrakcyjnych źródeł danych.
Bryan Boettcher
Sensowne może być zastosowanie dziedziczenia tylko ze względu na DRY i umieszczenie wspólnej implementacji w klasie bazowej. Ale nie wyrównałbyś żadnych przesłoniętych metod z żadnymi metodami interfejsu, klasa podstawowa zawierałaby ogólną logikę wsparcia. Gdyby się ułożyły, pokazałoby to, że dziedziczenie rozwiązuje twój problem, a interfejs nie miałby żadnego celu. Interfejs jest używany w przypadkach, gdy dziedziczenie nie rozwiązuje problemu, ponieważ wspólne cechy, które chcesz modelować, nie pasują do drzewa klasy singke. I tak, sformułowanie w tym przykładzie czyni to tym bardziej błędnym.
Martin Maat,