Interfacce su una classe astratta

28

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

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 istantaneabile dell'interfaccia. Completa completamente il 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()));
    }
}

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

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

    
posta Bryan Boettcher 04.09.2012 - 19:28
fonte

10 risposte

23

I'm of the belief that a class should not implement an interface unless that class can be used when an implementation of the interface is required.

BaseWorker soddisfa questo requisito. Solo perché non puoi istanziare direttamente un oggetto BaseWorker non significa che non puoi avere un BaseWorker puntatore che soddisfi il contratto. In effetti, questo è praticamente l'argomento delle lezioni astratte.

Inoltre, è difficile capire dall'esempio semplificato che hai postato, ma parte del problema potrebbe essere che l'interfaccia e la classe astratta sono ridondanti. A meno che tu non abbia altre classi che implementano IFooWorker che non derivano da BaseWorker , non hai affatto bisogno dell'interfaccia. Le interfacce sono solo classi astratte con alcune limitazioni aggiuntive che semplificano l'ereditarietà multipla.

Di nuovo, essendo difficile da spiegare da un esempio semplificato, l'uso di un metodo protetto, che fa riferimento esplicitamente alla base da una classe derivata e la mancanza di un luogo non ambiguo per dichiarare l'implementazione dell'interfaccia sono segnali di allarme che si sta usando in modo inappropriato eredità invece di composizione. Senza quell'eredità, la tua intera domanda diventa discutibile.

    
risposta data 04.09.2012 - 21:14
fonte
44

Dovrei essere d'accordo con il tuo collega.

In entrambi gli esempi forniti, BaseWorker definisce il metodo astratto Work (), 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 che l'implementazione sarebbe ereditata dalle sue sottoclassi. Ciò ti eviterà di dover indicare esplicitamente che ciascuna sottoclasse è effettivamente un IFooWorker (il principio di DRY).

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

    
risposta data 04.09.2012 - 19:37
fonte
11

In genere sono d'accordo con il tuo collega.

Prendiamo il tuo modello: l'interfaccia è implementata solo dalle classi figlie, anche se la classe base impone anche l'esposizione dei metodi IFooWorker. Prima di tutto, è ridondante; se la classe figlio implementa o meno l'interfaccia, è necessario ignorare i metodi esposti di BaseWorker e, analogamente, qualsiasi implementazione di IFooWorker deve esporre la funzionalità indipendentemente dal fatto che ottenga aiuto da BaseWorker o meno.

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

Il modello del tuo collaboratore è molto più facile da usare e da sostituire. "Tutti i BaseWorkers sono IFooWorkers" è ora una vera affermazione; puoi dare qualsiasi istanza di BaseWorker a qualsiasi dipendenza di IFooWorker, nessun problema. L'affermazione opposta "Tutti i lavoratori IFoo sono lavoratori di base" non è vera; che ti permette di sostituire BaseWorker con BetterBaseWorker e il tuo codice che consuma (che dipende da IFooWorker) non dovrà dire la differenza.

    
risposta data 04.09.2012 - 23:06
fonte
6

Devo aggiungere qualcosa di avvertimento a queste risposte. Unire la 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 potrebbe non essere vero in tutti i casi.

Assumi le classi del framework di raccolta Java:

abstract class AbstractList
class LinkedList extends AbstractList implements Queue

Il fatto che il contratto Queue sia implementato da LinkedList non ha spinto il problema in AbstractList .

Qual è la differenza tra i due casi? Lo scopo di BaseWorker era sempre (come comunicato dal suo nome e interfaccia) per implementare le operazioni in IWorker . Lo scopo di AbstractList e quello di Queue sono divergenti, ma un discendente del primo può ancora implementare quest'ultimo contratto.

    
risposta data 05.09.2012 - 00:35
fonte
1

Innanzitutto pensa a cosa sia una classe base astratta e quale sia un'interfaccia. Considera quando userai l'una o l'altra e quando non lo faresti.

È normale che le persone pensino che entrambi siano concetti molto simili, infatti è una domanda di intervista comune (la differenza tra i due è ??)

Quindi una classe base astratta ti dà qualcosa che le interfacce non possono essere le implementazioni di default dei metodi. (C'è dell'altro, 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 eliminabile. Puoi quindi giocare con diverse opzioni. Rendi Dispose astratto, costringendo le classi derivate a implementarlo. Fornire un'implementazione virtuale predefinita, in modo che non lo facciano, o non renderlo né virtuale né astratto e chiamarlo con metodi virtuali chiamati cose come BeforeDispose, AfterDispose, OnDispose o Disposing per esempio.

Quindi ogni volta che tutte le classi derivate devono supportare l'interfaccia, la classe base va avanti. Se solo uno o alcuni hanno bisogno di quell'interfaccia andrebbe sulla classe derivata.

Questa è tutta una semplificazione grossolana. Un'altra alternativa è che le classi derivate non implementino nemmeno le interfacce, ma le forniscono tramite un modello di adattatore. Un esempio di ciò che ho visto di recente era in IObjectContextAdapter.

    
risposta data 05.09.2012 - 01:02
fonte
1

Vorrei fare la domanda, cosa succede quando cambi IFooWorker , come aggiungere un nuovo metodo?

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

Per questo motivo, farei in modo che la classe base implementasse l'interfaccia , poiché I potrebbe essere in grado di implementare tutte le funzionalità per il nuovo metodo nella classe base senza toccare le classi derivate.

    
risposta data 05.09.2012 - 17:03
fonte
0

Sono ancora lontani anni dal cogliere appieno la distinzione tra una classe astratta e un'interfaccia. Ogni volta che penso di avere un controllo sui concetti di base, vado a cercare su stackexchange e sono indietro di due passi. Ma alcuni pensieri sull'argomento e la domanda sui PO:

Per prima cosa:

Ci sono due spiegazioni comuni di un'interfaccia:

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

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

Ci sono 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 Playboy Yacht sexy sarebbe una sottoclasse di BaseBoat.

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

A proposito, il libro Citers sembra sempre impressionante, anche se mi allontano ancora confuso.

Secondo:

Su SO, qualcuno ha chiesto una versione più semplice di questa domanda, il classico, "perché utilizzare le interfacce? Qual è la differenza? Cosa mi manca?" E una risposta ha utilizzato un pilota dell'aeronautica come un semplice esempio. Non è stato sufficiente, ma ha suscitato alcuni grandi commenti, uno dei quali menzionava l'interfaccia IFlyable con metodi come takeOff, pilotEject, ecc. E questo mi ha davvero fatto da esempio del mondo reale del perché le interfacce non sono solo utili, ma cruciale. Un'interfaccia rende un oggetto / classe intuitivo, o almeno dà la sensazione che sia. Un'interfaccia non è a vantaggio dell'oggetto o dei dati, ma per qualcosa che deve interagire con quell'oggetto. I classici esempi di ereditarietà di Fruit- > Apple- > Fuji o Shape- > Triangle- > Equilateral sono un grande modello per la comprensione tassonomica di un dato oggetto in base ai suoi discendenti. Informa il consumatore e i processori in merito alle sue qualità generali, ai suoi comportamenti, se l'oggetto è un gruppo di cose, snellirà il sistema se lo metti nel posto sbagliato, o descrive dati sensoriali, un connettore a un data store specifico, o dati finanziari necessari per rendere i salari.

Un oggetto Piano specifico può o non può avere un metodo per un atterraggio di emergenza, ma io sarò incazzato se presumo la sua emergenzaLand come ogni altro oggetto volabile solo per svegliarmi coperto in DumbPlane e apprendere che gli sviluppatori è andato con quickLand perché pensavano che fosse tutto uguale. Proprio come sarei frustrato se ogni produttore di viti avesse la propria interpretazione di possente o se il mio TV non avesse il pulsante di aumento del volume sopra il pulsante di diminuzione del volume.

Le classi astratte sono il modello che stabilisce ciò che qualsiasi oggetto discendente deve avere per qualificare quella classe. Se non hai tutte le qualità di un'anatra, non importa se hai implementato l'interfaccia IQuack, sei solo un pinguino strano. 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 volare astronavi aliene perché l'interfaccia era affidabile in modo simile.

Terzo:

Sono d'accordo con il tuo collega, perché a volte hai bisogno di applicare determinati metodi sin dall'inizio. Se si sta creando un ORM di record 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 accoppiato esclusivamente alla 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'è stato un ottimo esempio prima di quando non si è passati a legare un'interfaccia alla classe astratta, perché non tutti i tipi di lista implementeranno (o dovrebbero) implementare un'interfaccia di coda. Hai detto che questo scenario si verifica per metà tempo, il che significa che tu e il tuo collega avete entrambi metà del tempo sbagliato, e quindi la cosa migliore da fare è discutere, discutere, considerare, e se risultano essere giusti, riconoscere e accettare l'accoppiamento . Ma non diventare uno sviluppatore che segue una filosofia anche quando non è la migliore per il lavoro a portata di mano.

    
risposta data 15.05.2015 - 14:52
fonte
0

C'è un altro aspetto del perché il DbWorker dovrebbe implementare IFooWorker , come suggerisci.

In caso di stile del tuo collaboratore, se per qualche motivo si verifica un successivo refactoring e si ritiene che DbWorker non estenda la classe BaseWorker astratta, DbWorker perde l'interfaccia IFooWorker . Questo potrebbe, ma non necessariamente, avere un impatto sui client che lo consumano, se si aspettano l'interfaccia IFooWorker .

    
risposta data 03.10.2016 - 01:31
fonte
-1

Per iniziare, mi piace definire le interfacce e le 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é è ciò che il contratto richiede loro. Pertanto la tua classe base Baseworker dovrebbe implementare quel metodo in modo esplicito o come funzione virtuale. Ti suggerisco di mettere questo metodo nella tua classe base a causa del semplice fatto che tutti i lavoratori hanno bisogno di work() . Pertanto è logico che la tua classe di base (anche se non puoi creare un puntatore di quel tipo) la includa come funzione.

Ora, DbWorker soddisfa l'interfaccia IFooWorker , ma se c'è un modo specifico in cui questa classe fa work() , allora ha davvero bisogno di sovraccaricare la definizione ereditata da BaseWorker . La ragione per cui non dovrebbe implementare direttamente l'interfaccia IFooWorker è che questo non è qualcosa di speciale per DbWorker . Se lo hai fatto ogni volta che hai implementato classi simili a DbWorker , violerebbe DRY .

Se due classi implementano la stessa funzione in modi diversi, puoi iniziare a cercare la più grande superclasse comune. La maggior parte delle volte ne troverai uno. In caso contrario, continua a cercare o a rinunciare e accetta di non avere abbastanza cose in comune per creare una classe base.

    
risposta data 05.09.2012 - 02:51
fonte
-3

Wow, tutte queste risposte e nessuna indicano che l'interfaccia nell'esempio non ha alcuno scopo. Non usi l'ereditarietà e un'interfaccia per lo stesso metodo, è inutile. Tu usi l'una o l'altra. 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.

    
risposta data 03.10.2016 - 22:21
fonte