Interfacce su una classe astratta


30

Io e il mio collega abbiamo opinioni diverse sulla relazione tra classi di base e interfacce. Sono convinto che una classe non dovrebbe implementare un'interfaccia a meno che quella classe non possa essere utilizzata quando è richiesta un'implementazione dell'interfaccia. In altre parole, mi piace vedere il codice in questo modo:

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()));
    }
}

DbWorker è ciò che ottiene l'interfaccia IFooWorker, perché è un'implementazione istantanea dell'interfaccia. Adempie completamente al contratto. Il mio collega preferisce il quasi identico:

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()));
    }
}

Laddove la classe base ottiene l'interfaccia, e in virtù di ciò anche tutti gli eredi della classe base appartengono a tale interfaccia. Questo mi infastidisce ma non riesco a trovare ragioni concrete per cui, al di fuori di "la classe base non può stare da sola come implementazione dell'interfaccia".

Quali sono i pro e i contro del suo metodo contro il mio, e perché uno dovrebbe essere usato su un altro?


Il tuo suggerimento ricorda molto da vicino l'eredità del diamante , che può causare molta confusione più in basso.
Spoike,

@Spoike: come vederlo?
Niing

@Nando il link che ho fornito nel commento ha un semplice diagramma di classe e una semplice spiegazione di una riga a ciò che è. si chiama problema "a diamante" perché la struttura ereditaria è sostanzialmente disegnata come una forma a diamante (cioè ♦ ️).
Spoike,

@ Spoike: perché questa domanda causerà l'eredità dei diamanti?
Niing

1
@Niing: l'ereditarietà di BaseWorker e IFooWorker con lo stesso metodo chiamato Workè il problema dell'ereditarietà del diamante (in tal caso entrambi soddisfano un contratto sul Workmetodo). In Java ti viene richiesto di implementare i metodi dell'interfaccia in modo che il problema di quale Workmetodo il programma dovrebbe usare sia evitato in quel modo. Linguaggi come il C ++ tuttavia non chiariscono questo per te.
Spoike,

Risposte:


24

Sono convinto che una classe non dovrebbe implementare un'interfaccia a meno che quella classe non possa essere utilizzata quando è richiesta un'implementazione dell'interfaccia.

BaseWorkersoddisfa tale requisito. Solo perché non puoi istanziare direttamente un BaseWorkeroggetto non significa che non puoi avere un BaseWorker puntatore che soddisfi il contratto. In effetti, questo è praticamente il punto centrale delle classi astratte.

Inoltre, è difficile capire dall'esempio semplificato che hai pubblicato, ma parte del problema potrebbe essere che l'interfaccia e la classe astratta sono ridondanti. A meno che non si disponga di altre classi di implementazione IFooWorkerche non derivano BaseWorker, l'interfaccia non è necessaria. Le interfacce sono solo classi astratte con alcune limitazioni aggiuntive che semplificano l'ereditarietà multipla.

Ancora una volta difficile da dire da un esempio semplificato, l'uso di un metodo protetto, facendo esplicito riferimento alla base da una classe derivata, e la mancanza di un posto non ambiguo in cui dichiarare l'implementazione dell'interfaccia sono segnali di avvertimento che stai usando in modo inappropriato l'ereditarietà invece di composizione. Senza quell'eredità, l'intera domanda diventa discutibile.


1
+1 per aver sottolineato che solo perché BaseWorkernon è direttamente istanziabile, non significa che un dato BaseWorker myWorkernon si attui IFooWorker.
Avner Shahar-Kashtan,

2
Non sono d'accordo sul fatto che le interfacce siano solo classi astratte. Le classi astratte di solito definiscono alcuni dettagli di implementazione (altrimenti sarebbe stata utilizzata un'interfaccia). Se questi dettagli non sono di tuo gradimento, ora devi cambiare ogni utilizzo della classe base in qualcos'altro. Le interfacce sono superficiali; definiscono la relazione "plug-and-socket" tra dipendenze e dipendenti. Pertanto, l'accoppiamento stretto a qualsiasi dettaglio di implementazione non è più un problema. Se una classe che eredita l'interfaccia non è di tuo gradimento, eliminala e aggiungi qualcos'altro interamente; al tuo codice potrebbe importare di meno.
KeithS,

5
Entrambi presentano vantaggi, ma in genere l'interfaccia deve sempre essere utilizzata per la dipendenza, indipendentemente dal fatto che esista o meno una classe astratta che definisce le implementazioni di base. La combinazione di interfaccia e classe astratta offre il meglio di entrambi i mondi; l'interfaccia mantiene la relazione plug-and-socket di superficie superficiale, mentre la classe astratta fornisce l'utile codice comune. È possibile rimuovere e sostituire qualsiasi livello di questo sotto l'interfaccia a piacimento.
KeithS,

Per "puntatore" intendi un'istanza di un oggetto (a cui un puntatore farebbe riferimento)? Le interfacce non sono classi, sono tipi e possono agire come sostituti incompleti dell'ereditarietà multipla, non una sostituzione, ovvero "includono il comportamento di più origini in una classe".
Samis,

46

Dovrei essere d'accordo con il tuo collega.

In entrambi gli esempi forniti, BaseWorker definisce il metodo astratto Work (), il che significa che tutte le sottoclassi sono in grado di soddisfare il contratto di IFooWorker. In questo caso, penso che BaseWorker dovrebbe implementare l'interfaccia e tale implementazione sarebbe ereditata dalle sue sottoclassi. Questo ti eviterà di dover indicare esplicitamente che ogni sottoclasse è effettivamente un IFooWorker (il principio DRY).

Se non stavi definendo Work () come metodo di BaseWorker o se IFooWorker avesse altri metodi che le sottoclassi di BaseWorker non volevano o non avrebbero bisogno, allora (ovviamente) dovresti indicare quali sottoclassi implementano effettivamente IFooWorker.


11
+1 avrebbe pubblicato la stessa cosa. Mi dispiace insta, ma penso che anche il tuo collega abbia ragione in questo caso, se qualcosa garantisce di adempiere a un contratto, dovrebbe ereditare quel contratto, indipendentemente dal fatto che la garanzia sia soddisfatta da un'interfaccia, una classe astratta o una classe concreta. In poche parole: il garante dovrebbe ereditare il contratto che garantisce.
Jimmy Hoffa,

2
Sono generalmente d'accordo, ma vorrei sottolineare che parole come "base", "standard", "predefinito", "generico", ecc. Sono odori di codice nel nome di una classe. Se una classe base astratta ha quasi lo stesso nome di un'interfaccia ma con una di quelle parole donnole incluse, è spesso un segno di astrazione errata; le interfacce definiscono ciò che fa qualcosa , il tipo ereditarietà definisce ciò che è . Se hai un IFooe un BaseFooallora significa che o IFoonon è un'interfaccia a grana fine adeguatamente, o che BaseFoosta usando l'ereditarietà in cui la composizione potrebbe essere una scelta migliore.
Aaronaught,

@Aaronaught Un buon punto, anche se può darsi che ci sia davvero qualcosa che tutte o la maggior parte delle implementazioni di IFoo devono ereditare (come un handle per un servizio o l'implementazione DAO).
Matthew Flynn,

2
Quindi la classe base non dovrebbe essere chiamata MyDaoFooo SqlFooo HibernateFooqualsiasi altra cosa, per indicare che si tratta solo di un possibile albero delle classi che si sta implementando IFoo? Per me è ancora più un odore se una classe base è accoppiata a una libreria o piattaforma specifica e non si fa menzione di questo nel nome ...
Aaronaught

@Aaronaught - Sì. Sono completamente d'accordo.
Matthew Flynn,

11

Sono generalmente d'accordo con il tuo collega.

Prendiamo il tuo modello: l'interfaccia è implementata solo dalle classi figlio, anche se la classe base impone anche l'esposizione dei metodi IFooWorker. Prima di tutto, è ridondante; indipendentemente dal fatto che la classe figlio implementi o meno l'interfaccia, sono tenuti a sovrascrivere i metodi esposti di BaseWorker e allo stesso modo qualsiasi implementazione di IFooWorker deve esporre la funzionalità indipendentemente dal fatto che ottengano aiuto da BaseWorker o meno.

Questo rende inoltre ambigua la tua gerarchia; "Tutti gli IFooWorker sono BaseWorker" non è necessariamente una vera affermazione, e nemmeno "Tutti i BaseWorker sono IFooWorker". Quindi, se si desidera definire una variabile di istanza, un parametro o un tipo restituito che potrebbe essere qualsiasi implementazione di IFooWorker o BaseWorker (sfruttando la funzionalità esposta comune che è uno dei motivi per ereditare in primo luogo), nessuno dei questi sono garantiti per essere onnicomprensivi; alcuni BaseWorker non saranno assegnabili a una variabile di tipo IFooWorker e viceversa.

Il modello del tuo collega è molto più facile da usare e da sostituire. "Tutti i BaseWorker sono IFooWorker" è ora una vera affermazione; puoi dare qualsiasi istanza di BaseWorker a qualsiasi dipendenza di IFooWorker, senza problemi. L'affermazione opposta "Tutti gli IFooWorker sono BaseWorker" non è vera; che ti consente di sostituire BaseWorker con BetterBaseWorker e il tuo codice di consumo (che dipende da IFooWorker) non dovrà fare la differenza.


Mi piace il suono di questa risposta, ma non seguo abbastanza l'ultima parte. Stai suggerendo che BetterBaseWorker sarebbe una classe astratta indipendente che implementa IFooWorker, in modo che il mio ipotetico plugin possa semplicemente dire "oh boy! The Better Base Worker è finalmente uscito!" e cambiare i riferimenti di BaseWorker a BetterBaseWorker e chiamarlo un giorno (magari giocare con i nuovi fantastici valori restituiti che mi permettono di eliminare enormi blocchi del mio codice ridondante, ecc.)?
Anthony,

Più o meno sì. Il grande vantaggio è che ridurrete il numero di riferimenti a BaseWorker che dovrà invece essere BetterBaseWorkers; la maggior parte del tuo codice non farà riferimento direttamente alla classe concreta, invece usando IFooWorker, quindi quando cambi ciò che è assegnato a quelle dipendenze IFooWorker (proprietà di classi, parametri di costruttori o metodi) il codice che utilizza IFooWorker non dovrebbe vedere alcuna differenza in uso.
KeithS,

6

Devo aggiungere qualcosa di un avvertimento a queste risposte. L'accoppiamento della classe base all'interfaccia crea una forza nella struttura di quella classe. Nel tuo esempio di base, è un gioco da ragazzi che i due dovrebbero essere accoppiati, ma ciò potrebbe non essere vero in tutti i casi.

Prendi le classi del framework di raccolta Java:

abstract class AbstractList
class LinkedList extends AbstractList implements Queue

Il fatto che il contratto di coda sia implementato da LinkedList non ha spinto la preoccupazione in AbstractList .

Qual è la distinzione tra i due casi? Lo scopo di BaseWorker era sempre (come comunicato dal suo nome e dalla sua interfaccia) di implementare operazioni in IWorker . Lo scopo di AbstractList e quello di Queue sono divergenti, ma un discendente del primo può ancora implementare il secondo contratto.


Questo succede metà del tempo. Preferisce sempre implementare l'interfaccia sulla classe base e preferisco sempre implementarla sul calcestruzzo finale. Lo scenario che hai presentato accade spesso e fa parte del motivo per cui le interfacce sulla base mi danno fastidio.
Bryan Boettcher,

1
Giusto, insta, e considero l'uso delle interfacce in questo modo come un aspetto dell'abuso di eredità che vediamo in molti ambienti di sviluppo. Come affermato dal GoF nel loro libro di schemi di progettazione, preferire la composizione rispetto all'eredità e mantenere l'interfaccia al di fuori della classe di base è uno dei modi per promuovere proprio questo principio.
Mihai Danila,

1
Ricevo l'avvertimento, ma noterai che la classe astratta dell'OP includeva metodi astratti corrispondenti all'interfaccia: un BaseWorker è implicitamente un IFooWorker. Renderlo esplicito rende questo fatto più utilizzabile. Esistono numerosi metodi inclusi in Queue che non sono inclusi in AbstractList, tuttavia, e come tali, un AbstractList non è una coda, anche se i suoi figli potrebbero esserlo. AbstractList e Queue sono ortogonali.
Matthew Flynn,

Grazie per questa intuizione; Sono d'accordo sul fatto che il caso del PO rientri in quella categoria, ma ho sentito che c'era una discussione più ampia alla ricerca di una regola più profonda, e volevo condividere le mie osservazioni su una tendenza di cui ho letto (e persino che ho avuto me stesso per un breve periodo) di uso eccessivo dell'eredità, forse perché quello è uno degli strumenti nella cassetta degli attrezzi di OOP.
Mihai Danila,

Dang, sono passati sei anni. :)
Mihai Danila,

2

Vorrei porre la domanda, cosa succede quando si cambia IFooWorker, come l'aggiunta di un nuovo metodo?

Se BaseWorkerimplementa l'interfaccia, per impostazione predefinita dovrà dichiarare il nuovo metodo, anche se è astratto. D'altra parte, se non implementa l'interfaccia, otterrai solo errori di compilazione sulle classi derivate.

Per questo motivo, farei implementare l'interfaccia alla classe base , poiché potrei essere in grado di implementare tutte le funzionalità per il nuovo metodo nella classe base senza toccare le classi derivate.


1

Per prima cosa, pensa a cosa sia una classe base astratta e che cos'è un'interfaccia. Considera quando useresti l'uno o l'altro e quando non lo faresti.

È comune per le persone pensare che entrambi siano concetti molto simili, in realtà è una domanda di intervista comune (la differenza tra i due è ??)

Quindi una classe base astratta ti dà qualcosa che le interfacce non possono essere implementazioni predefinite di metodi. (Ci sono altre cose, in C # puoi avere metodi statici su una classe astratta, non su un'interfaccia per esempio).

Ad esempio, un uso comune di questo è con IDisposable. La classe astratta implementa il metodo Dispose di IDisposable, il che significa che qualsiasi versione derivata della classe base sarà automaticamente disponibile. Puoi quindi giocare con diverse opzioni. Rendi astratto Dispose, forzando le classi derivate a implementarlo. Fornire un'implementazione virtuale predefinita, in modo che non lo facciano, o renderla né virtuale o astratta e farla chiamare metodi virtuali chiamati cose come BeforeDispose, AfterDispose, OnDispose o Disposing per esempio.

Quindi ogni volta che tutte le classi derivate devono supportare l'interfaccia, passa alla classe base. Se solo una o alcune necessitino di tale interfaccia, andrebbe sulla classe derivata.

Questa è in realtà una semplificazione eccessiva. Un'altra alternativa è che le classi derivate non implementino nemmeno le interfacce, ma le forniscano tramite un modello di adattatore. Un esempio di ciò che ho visto di recente è stato in IObjectContextAdapter.


1

Mi mancano ancora anni per comprendere appieno la distinzione tra una classe astratta e un'interfaccia. Ogni volta che penso di avere un'idea dei concetti di base, vado a cercare stackexchange e faccio due passi indietro. Ma alcuni pensieri sull'argomento e sulla domanda dei PO:

Primo:

Esistono due spiegazioni comuni di un'interfaccia:

  1. Un'interfaccia è un elenco di metodi e proprietà che qualsiasi classe può implementare e, implementando un'interfaccia, una classe garantisce che quei metodi (e le loro firme) e quelle proprietà (e i loro tipi) saranno disponibili quando "si interfaccia" con quella classe o un oggetto di quella classe. Un'interfaccia è un contratto.

  2. Le interfacce sono classi astratte che non possono / non possono fare nulla. Sono utili perché puoi implementarne più di uno, a differenza di quelle classi genitoriali. Ad esempio, potrei essere un oggetto della classe BananaBread ed ereditare da BaseBread, ma ciò non significa che non sia possibile implementare anche le interfacce IWithNuts e ITastesYummy. Potrei persino implementare l'interfaccia IDoesTheDishes, perché non sono solo pane, sai?

Esistono due spiegazioni comuni di una classe astratta:

  1. Una classe astratta è, sai, la cosa non è ciò che la cosa può fare. È come, l'essenza, non proprio una cosa reale. Aspetta, questo ti aiuterà. Una barca è una classe astratta, ma un sexy Playboy Yacht sarebbe una sottoclasse di BaseBoat.

  2. Ho letto un libro su lezioni astratte e forse dovresti leggere quel libro, perché probabilmente non lo capisci e lo sbaglierai se non lo hai letto.

A proposito, il libro Citers sembra sempre impressionante, anche se me ne vado ancora confuso.

Secondo:

Su SO, qualcuno ha posto una versione più semplice di questa domanda, la classica, "perché usare le interfacce? Qual è la differenza? Cosa mi manca?" E una risposta ha usato un pilota dell'aeronautica come semplice esempio. Non è riuscito, ma ha suscitato alcuni grandi commenti, uno dei quali ha menzionato l'interfaccia IFlyable con metodi come takeOff, pilotEject, ecc. E questo mi ha davvero cliccato come un esempio reale del perché le interfacce non sono solo utili, ma cruciale. Un'interfaccia rende un oggetto / classe intuitivo, o almeno dà il senso che lo sia. Un'interfaccia non è a beneficio dell'oggetto o dei dati, ma per qualcosa che deve interagire con quell'oggetto. Il classico frutto-> mela-> fuji o forma-> triangolo-> Esempi equilaterali di ereditarietà sono un ottimo modello per comprendere tassonomicamente un determinato oggetto in base ai suoi discendenti. Informa il consumatore e i processori sulle sue qualità generali, i comportamenti, se l'oggetto è un gruppo di cose, mancherà il tuo sistema se lo metti nel posto sbagliato, o descrivi i dati sensoriali, un connettore in un archivio dati specifico, o dati finanziari necessari per rendere il libro paga.

Un oggetto Plane specifico può avere o meno un metodo per un atterraggio di emergenza, ma sarò incazzato se assumo la sua EmergencyLand come ogni altro oggetto volante solo per svegliarmi coperto in DumbPlane e imparare che gli sviluppatori sono andati con quickLand perché pensavano che fosse tutto uguale. Proprio come sarei frustrato se ogni produttore di viti avesse la propria interpretazione di righty tighty o se la mia TV non avesse il pulsante di aumento del volume sopra il pulsante di riduzione del volume.

Le classi astratte sono il modello che stabilisce ciò che qualsiasi oggetto discendente deve avere per qualificarsi come quella classe. Se non hai tutte le qualità di un'anatra, non importa che tu abbia implementato l'interfaccia IQuack, sei solo uno strano pinguino. Le interfacce sono le cose che hanno senso anche quando non puoi essere sicuro di nient'altro. Jeff Goldblum e Starbuck erano entrambi in grado di pilotare astronavi aliene perché l'interfaccia era affidabile in modo affidabile.

Terzo:

Sono d'accordo con il tuo collega, perché a volte è necessario applicare determinati metodi all'inizio. Se stai creando un record ORM attivo, è necessario un metodo di salvataggio. Questo non dipende dalla sottoclasse che può essere istanziata. E se l'interfaccia ICRUD è abbastanza portatile da non essere accoppiata esclusivamente all'unica classe astratta, può essere implementata da altre classi per renderle affidabili e intuitive per chiunque abbia già familiarità con una delle classi discendenti di quella classe astratta.

D'altra parte, c'era un ottimo esempio prima di quando non saltare a legare un'interfaccia alla classe astratta, perché non tutti i tipi di elenco implementeranno (o dovrebbero) un'interfaccia di coda. Hai detto che questo scenario si verifica metà del tempo, il che significa che tu e il tuo collega avete entrambi torto nella metà del tempo, e quindi la cosa migliore da fare è discutere, discutere, considerare e se si rivelano giusti, riconoscere e accettare l'accoppiamento . Ma non diventare uno sviluppatore che segue una filosofia anche quando non è la migliore per il lavoro da svolgere.


0

C'è un altro aspetto del perché l' DbWorkerimplementazione dovrebbe IFooWorker, come suggerisci.

Nel caso dello stile del tuo collega, se per qualche motivo si verifica un successivo refactoring e DbWorkersi ritiene che non estenda la BaseWorkerclasse astratta , DbWorkerperde l' IFooWorkerinterfaccia. Ciò potrebbe, ma non necessariamente, avere un impatto sui clienti che lo consumano, se si aspettano l' IFooWorkerinterfaccia.


-1

Per iniziare, mi piace definire interfacce e classi astratte in modo diverso:

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)

Nel tuo caso, tutti i lavoratori implementano work()perché questo è ciò che il contratto richiede loro. Pertanto la tua classe base Baseworkerdovrebbe implementare quel metodo in modo esplicito o come funzione virtuale. Ti suggerirei di inserire questo metodo nella tua classe base per il semplice fatto che tutti i lavoratori devono fare work(). Pertanto è logico che la tua baseclass (anche se non puoi creare un puntatore di quel tipo) la includa come una funzione.

Ora DbWorkersoddisfa l' IFooWorkerinterfaccia, ma se esiste un modo specifico per questa classe work(), allora ha davvero bisogno di sovraccaricare la definizione ereditata BaseWorker. Il motivo per cui non dovrebbe implementare IFooWorkerdirettamente l'interfaccia è che questo non è qualcosa di speciale DbWorker. Se lo facessi ogni volta che implementi classi simili a quelle, DbWorkerallora violeresti DRY .

Se due classi implementano la stessa funzione in modi diversi, potresti iniziare a cercare la più grande superclasse comune. Il più delle volte ne troverai uno. Altrimenti, continua a cercare o arrenditi e accetta di non avere abbastanza cose in comune per creare una classe base.


-3

Caspita, tutte queste risposte e nessuna sottolineando che l'interfaccia nell'esempio non serve a nulla. Non usi l'ereditarietà e un'interfaccia per lo stesso metodo, è inutile. Usi l'uno o l'altro. Ci sono molte domande su questo forum con risposte che spiegano i vantaggi di ciascuno e dei senari che richiedono l'uno o l'altro.


3
Questa è una falsità brevettuale. L'interfaccia è un contratto in base al quale il sistema dovrebbe comportarsi, indipendentemente dall'implementazione. La classe base ereditata è una delle molte implementazioni possibili per questa interfaccia. Un sistema in scala abbastanza grande avrà più classi base che soddisfano le varie interfacce (generalmente chiuse con generici), che è esattamente ciò che ha fatto questa configurazione e perché è stato utile dividerle.
Bryan Boettcher,

È davvero un cattivo esempio dal punto di vista OOP. Come dici "la classe base è solo un'implementazione" (dovrebbe essere o non avrebbe senso per l'interfaccia), stai dicendo che ci saranno classi non worker che implementano l'interfaccia IFooWorker. Quindi avrai una classe base che è un lavoratore specializzato (dovremmo presumere che ci possano essere anche baristi) e avresti altri collaboratori che non sono nemmeno lavoratori di base! Questo è all'indietro. In più di un modo.
Martin Maat,

1
Forse sei bloccato sulla parola "Lavoratore" nell'interfaccia. Che dire di "Origine dati"? Se abbiamo un IDatasource <T>, possiamo banalmente avere un SqlDatasource <T>, RestDatasource <T>, MongoDatasource <T>. L'azienda decide quali chiusure dell'interfaccia generica vanno alle implementazioni di chiusura delle origini dati astratte.
Bryan Boettcher,

Può avere senso usare l'ereditarietà solo per motivi di DRY e mettere una implementazione comune in una classe base. Ma non allineare alcun metodo ignorato con alcun metodo di interfaccia, la classe base conterrebbe una logica di supporto generale. Se si allineassero, ciò mostrerebbe che l'eredità risolve il tuo problema e l'interfaccia non servirebbe a nulla. Si utilizza un'interfaccia nel caso in cui l'ereditarietà non risolva il problema perché le caratteristiche comuni che si desidera modellare non si adattano a un albero di classi singke. E sì, la formulazione nell'esempio rende ancora più sbagliato.
Martin Maat,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.