Domanda

Quando dovrei usare un'interfaccia e quando dovrei usare una classe base?

Dovrebbe essere sempre un'interfaccia se non voglio effettivamente definire un'implementazione di base dei metodi?

Se ho una lezione su cani e gatti.Perché dovrei implementare IPet invece di PetBase?Posso capire la presenza di interfacce per ISheds o IBarks (IMakesNoise?), perché queste possono essere posizionate su un animale domestico per animale domestico, ma non capisco quale utilizzare per un animale domestico generico.

È stato utile?

Soluzione

Prendiamo l'esempio di una classe Dog and a Cat e illustriamo l'utilizzo di C#:

Sia un cane che un gatto sono animali, in particolare mammiferi quadrupedi (gli animali sono mooolto troppo generici).Supponiamo di avere una classe astratta Mammifero, per entrambi:

public abstract class Mammal

Questa classe base avrà probabilmente metodi predefiniti come:

  • Foraggio
  • Compagno

Tutti comportamenti che hanno più o meno la stessa implementazione tra le due specie.Per definirlo avrai:

public class Dog : Mammal
public class Cat : Mammal

Supponiamo ora che ci siano altri mammiferi, che solitamente vedremo allo zoo:

public class Giraffe : Mammal
public class Rhinoceros : Mammal
public class Hippopotamus : Mammal

Ciò sarà comunque valido perché è al centro della funzionalità Feed() E Mate() sarà ancora lo stesso.

Tuttavia, giraffe, rinoceronti e ippopotami non sono esattamente animali da trasformare in animali domestici.Ecco dove un'interfaccia sarà utile:

public interface IPettable
{
    IList<Trick> Tricks{get; set;}
    void Bathe();
    void Train(Trick t);
}

L'attuazione del contratto di cui sopra non sarà la stessa tra un cane e un gatto;mettere le loro implementazioni in una classe astratta da ereditare sarebbe una cattiva idea.

Le definizioni di Cane e Gatto ora dovrebbero assomigliare a:

public class Dog : Mammal, IPettable
public class Cat : Mammal, IPettable

Teoricamente puoi sovrascriverli da una classe base superiore, ma essenzialmente un'interfaccia ti consente di aggiungere solo le cose di cui hai bisogno in una classe senza bisogno di ereditarietà.

Di conseguenza, poiché di solito puoi ereditare solo da una classe astratta (nella maggior parte dei linguaggi OO tipizzati staticamente che è...le eccezioni includono C++) ma essere in grado di implementare più interfacce, consente di costruire oggetti in modo rigoroso come richiesto base.

Altri suggerimenti

Ebbene, si è detto Josh Bloch Java 2d efficace:

Preferire le interfacce alle classi astratte

Alcuni punti principali:

  • Le classi esistenti possono essere facilmente adattate per implementare una nuova interfaccia.Tutto quello che devi fare è aggiungere i metodi richiesti se non esistono ancora e aggiungono una clausola implementa alla dichiarazione di classe.

  • Le interfacce sono ideali per definire i mixin.Parlando vagamente, una mixin è un tipo che una classe può implementare oltre al suo "tipo primario" per dichiarare che fornisce un comportamento opzionale.Ad esempio, comparabile è un'interfaccia Mixin che consente a una classe di dichiarare che le sue istanze sono ordinate rispetto ad altri oggetti reciprocamente comparabili.

  • Le interfacce consentono la costruzione di framework di tipo non gerarchico.Le gerarchie di tipo sono perfette per organizzare alcune cose, ma altre cose non cadono in modo ordinato in una rigida gerarchia.

  • Le interfacce consentono miglioramenti delle funzionalità sicuri e potenti tramite il linguaggio di classe per la classe.Se usi classi astratte per definire i tipi, lasci il programmatore che desidera aggiungere funzionalità senza alternativa ma per usare l'eredità.

Inoltre, è possibile combinare le virtù delle interfacce e delle classi astratte fornendo una classe di implementazione scheletrica astratta per accompagnare ogni interfaccia non banale che esporta.

D'altra parte, le interfacce sono molto difficili da evolvere.Se aggiungi un metodo a un'interfaccia, tutte le sue implementazioni verranno interrotte.

P.S.:Acquista il libro.È molto più dettagliato.

Lo stile moderno è quello di definire IPet E PetBase.

Il vantaggio dell'interfaccia è che altro codice può utilizzarla senza alcun legame con altro codice eseguibile.Completamente "pulito". Anche le interfacce possono essere miscelate.

Ma le classi base sono utili per implementazioni semplici e utilità comuni.Quindi fornisci anche una classe base astratta per risparmiare tempo e codice.

Le interfacce e le classi base rappresentano due diverse forme di relazioni.

Eredità (classi base) rappresentano una relazione "is-a".Per esempio.un cane o un gatto "è un" animale domestico.Questa relazione rappresenta sempre il (singolo) scopo della classe (in concomitanza con il "principio di responsabilità unica").

Interfacce, d'altra parte, rappresentano caratteristiche aggiuntive di una classe.La definirei una relazione "è", come in "Foo è usa e getta", da qui il IDisposable interfaccia in C#.

Interfacce

  • Definisce il contratto tra 2 moduli.Non è possibile avere alcuna implementazione.
  • La maggior parte dei linguaggi consente di implementare più interfacce
  • La modifica di un'interfaccia è una modifica sostanziale.Tutte le implementazioni devono essere ricompilate/modificate.
  • Tutti i membri sono pubblici.Le implementazioni devono implementare tutti i membri.
  • Le interfacce aiutano nel disaccoppiamento.Puoi utilizzare framework fittizi per deridere qualsiasi cosa dietro un'interfaccia
  • Le interfacce normalmente indicano un tipo di comportamento
  • Le implementazioni dell'interfaccia sono disaccoppiate/isolate l'una dall'altra

Classi base

  • Ti permette di aggiungerne alcuni predefinito implementazione che ottieni gratuitamente per derivazione
  • Ad eccezione di C++, puoi derivare solo da una classe.Anche se potessero appartenere a più classi, di solito è una cattiva idea.
  • Cambiare la classe base è relativamente semplice.Le derivazioni non devono fare nulla di speciale
  • Le classi base possono dichiarare funzioni protette e pubbliche a cui è possibile accedere tramite derivazioni
  • Abstract Le classi base non possono essere derise facilmente come le interfacce
  • Le classi base normalmente indicano la gerarchia dei tipi (IS A)
  • Le derivazioni delle classi possono dipendere da alcuni comportamenti di base (avere una conoscenza complessa dell'implementazione del genitore).Le cose possono complicarsi se si apporta una modifica all'implementazione di base per un ragazzo e si interrompono gli altri.

In generale, dovresti favorire le interfacce rispetto alle classi astratte.Uno dei motivi per utilizzare una classe astratta è se si dispone di un'implementazione comune tra le classi concrete.Naturalmente, dovresti comunque dichiarare un'interfaccia (IPet) e fare in modo che una classe astratta (PetBase) implementi quell'interfaccia. Usando interfacce piccole e distinte, puoi usarne multipli per migliorare ulteriormente la flessibilità.Le interfacce consentono la massima flessibilità e portabilità dei tipi oltre i confini.Quando si passano i riferimenti oltre i confini, passare sempre l'interfaccia e non il tipo concreto.Ciò consente al destinatario di determinare l'implementazione concreta e fornisce la massima flessibilità.Questo è assolutamente vero quando si programma in modalità TDD/BDD.

La Gang of Four ha affermato nel loro libro "Poiché l'ereditarietà espone una sottoclasse ai dettagli dell'implementazione del suo genitore, si dice spesso che" l'ereditarietà interrompe l'incapsulamento".Credo che questo sia vero.

Questo è piuttosto specifico per .NET, ma il libro Linee guida per la progettazione del framework sostiene che in generale le classi offrono maggiore flessibilità in un framework in evoluzione.Una volta spedita un'interfaccia, non hai la possibilità di modificarla senza violare il codice che utilizzava quell'interfaccia.Con una classe, tuttavia, puoi modificarla e non interrompere il codice ad essa collegato.Finché apporti le modifiche giuste, inclusa l'aggiunta di nuove funzionalità, sarai in grado di estendere ed evolvere il tuo codice.

Krzysztof Cwalina dice a pagina 81:

Nel corso delle tre versioni di .NET Framework, ho parlato di questa linea guida con diversi sviluppatori del nostro team.Molti di loro, compresi quelli che inizialmente non erano d'accordo con le linee guida, hanno affermato di pentirsi di aver fornito alcune API come interfaccia.Non ho sentito nemmeno un caso in cui qualcuno si sia pentito di aver spedito una lezione.

Detto questo c'è sicuramente spazio per le interfacce.Come linea guida generale, fornire sempre un'implementazione astratta della classe base di un'interfaccia se non altro come esempio di un modo per implementare l'interfaccia.Nel migliore dei casi quella classe base farà risparmiare molto lavoro.

Juan,

Mi piace pensare alle interfacce come un modo per caratterizzare una classe.Una particolare classe di razza di cane, ad esempio uno YorkshireTerrier, può discendere dalla classe del cane genitore, ma implementa anche IFurry, IStubby e IYippieDog.Quindi la classe definisce cos'è la classe ma l'interfaccia ci dice cose al riguardo.

Il vantaggio è che mi permette, ad esempio, di raccogliere tutti gli IYippieDog e inserirli nella mia collezione Ocean.Quindi ora posso raggiungere un particolare insieme di oggetti e trovare quelli che soddisfano i criteri che sto osservando senza ispezionare troppo da vicino la classe.

Trovo che le interfacce dovrebbero davvero definire un sottoinsieme del comportamento pubblico di una classe.Se definisce tutto il comportamento pubblico per tutte le classi che lo implementano, in genere non è necessario che esista.Non mi dicono nulla di utile.

Questo pensiero però va contro l'idea che ogni classe dovrebbe avere un'interfaccia e che dovresti codificare l'interfaccia.Va bene, ma ti ritroverai con molte interfacce uno a uno per le classi e questo crea confusione.Capisco che l'idea è che non costa davvero nulla e ora puoi scambiare le cose dentro e fuori con facilità.Tuttavia, trovo che lo faccio raramente.La maggior parte delle volte sto semplicemente modificando la classe esistente e riscontro esattamente gli stessi problemi che ho sempre avuto se è necessario modificare l'interfaccia pubblica di quella classe, tranne che ora devo cambiarla in due punti.

Quindi se la pensi come me diresti sicuramente che Gatto e Cane sono IPettable.È una caratterizzazione che corrisponde a entrambi.

L'altro punto è che dovrebbero avere la stessa classe base?La domanda è se debbano essere trattati in generale come la stessa cosa.Certamente sono entrambi animali, ma è adatto al modo in cui li useremo insieme.

Supponiamo che io voglia raccogliere tutte le classi di animali e metterle nel contenitore dell'Arca.

O devono essere mammiferi?Forse abbiamo bisogno di una sorta di fabbrica di mungitura per animali incrociati?

Hanno addirittura bisogno di essere collegati insieme?È sufficiente sapere che sono entrambi IPettable?

Spesso sento il desiderio di ricavare un'intera gerarchia di classi quando in realtà mi serve solo una classe.Lo faccio in previsione che un giorno potrei averne bisogno e di solito non lo faccio mai.Anche quando lo faccio, di solito scopro che devo fare molto per risolverlo.Questo perché la prima classe che sto creando non è il Cane, non sono così fortunato, è invece l'Ornitorinco.Ora tutta la mia gerarchia di classi è basata sul caso bizzarro e ho molto codice sprecato.

Ad un certo punto potresti anche scoprire che non tutti i gatti sono pettable (come quello senza peli).Ora puoi spostare l'interfaccia su tutte le classi derivate adatte.Scoprirai che un cambiamento molto meno importante è che all'improvviso i gatti non derivano più da PettableBase.

Ecco la definizione base e semplice di interfaccia e classe base:

  • Classe base = ereditarietà dell'oggetto.
  • Interfaccia = eredità funzionale.

saluti

Raccomando di utilizzare la composizione anziché l'ereditarietà quando possibile.Utilizzare le interfacce ma utilizzare gli oggetti membro per l'implementazione di base.In questo modo, puoi definire una factory che costruisce i tuoi oggetti in modo che si comportino in un certo modo.Se vuoi cambiare il comportamento, crea un nuovo metodo factory (o factory astratta) che crea diversi tipi di sottooggetti.

In alcuni casi, potresti scoprire che i tuoi oggetti primari non necessitano affatto di interfacce, se tutto il comportamento modificabile è definito negli oggetti helper.

Quindi, invece di IPet o PetBase, potresti ritrovarti con un Pet che ha un parametro IFurBehavior.Il parametro IFurBehavior è impostato dal metodo CreateDog() di PetFactory.È questo parametro che viene chiamato per il metodo shed().

Se lo fai, scoprirai che il tuo codice è molto più flessibile e la maggior parte dei tuoi oggetti semplici gestisce comportamenti di base a livello di sistema.

Raccomando questo modello anche nei linguaggi con ereditarietà multipla.

Spiegato bene in questo Articolo sul mondo Java

Personalmente tendo a utilizzare le interfacce per definire le interfacce, ad es.parti della progettazione del sistema che specificano come si dovrebbe accedere a qualcosa.

Non è raro che avrò una classe che implementa 1 o più interfacce.

Classi astratte che utilizzo come base per qualcos'altro.

Quello che segue è un estratto dell'articolo sopra citato Articolo su JavaWorld.com, autore Tony Sintes, 20/04/01


Interfaccia vs.classe astratta

La scelta di interfacce e classi astratte non è una proposta o/o.Se hai bisogno di cambiare il tuo design, rendilo un'interfaccia.Tuttavia, potresti avere classi astratte che forniscono alcuni comportamenti predefiniti.Le classi astratte sono ottimi candidati all'interno dei framework applicativi.

Le classi astratte ti consentono di definire alcuni comportamenti;costringono le tue sottoclassi a fornirne altre.Ad esempio, se si dispone di un framework applicativo, una classe astratta può fornire servizi predefiniti come la gestione di eventi e messaggi.Tali servizi consentono alla tua applicazione di collegarsi al framework dell'applicazione.Tuttavia, esistono alcune funzionalità specifiche dell'applicazione che solo la tua applicazione può eseguire.Tali funzionalità potrebbero includere attività di avvio e arresto, che spesso dipendono dall'applicazione.Quindi, invece di provare a definire quel comportamento stesso, la classe base astratta può dichiarare metodi astratti di arresto e avvio.La classe base sa che ha bisogno di quei metodi, ma una classe astratta lascia che la tua classe ammetta di non sapere come eseguire quelle azioni;sa solo che deve avviare le azioni.Al momento dell'avvio, la classe astratta può chiamare il metodo startup.Quando la classe base chiama questo metodo, Java chiama il metodo definito dalla classe figlia.

Molti sviluppatori dimenticano che una classe che definisce un metodo astratto può chiamare anche quel metodo.Le classi astratte rappresentano un modo eccellente per creare gerarchie di ereditarietà pianificate.Sono anche una buona scelta per le classi nonleaf nelle gerarchie di classi.

Classe controinterfaccia

Alcuni dicono che dovresti definire tutte le classi in termini di interfacce, ma penso che la raccomandazione sembri un po' estrema.Utilizzo le interfacce quando vedo che qualcosa nel mio progetto cambierà frequentemente.

Ad esempio, il modello Strategy ti consente di scambiare nuovi algoritmi e processi nel tuo programma senza alterare gli oggetti che li utilizzano.Un lettore multimediale potrebbe sapere come riprodurre CD, MP3 e file wav.Naturalmente, non vuoi codificare questi algoritmi di riproduzione nel lettore;ciò renderà difficile aggiungere un nuovo formato come AVI.Inoltre, il tuo codice sarà disseminato di istruzioni case inutili.E per aggiungere la beffa al danno, dovrai aggiornare le dichiarazioni dei casi ogni volta che aggiungi un nuovo algoritmo.Tutto sommato, questo non è un modo di programmare molto orientato agli oggetti.

Con il pattern Strategy puoi semplicemente incapsulare l'algoritmo dietro un oggetto.In questo modo potrai fornire nuovi plug-in multimediali in qualsiasi momento.Chiamiamo la classe plug-in MediaStrategy.Quell'oggetto avrebbe un metodo:playStream(Streaming).Quindi, per aggiungere un nuovo algoritmo, estendiamo semplicemente la nostra classe dell'algoritmo.Ora, quando il programma incontra il nuovo tipo di media, delega semplicemente la riproduzione dello streaming alla nostra strategia mediatica.Naturalmente, avrai bisogno di alcuni strumenti idraulici per istanziare correttamente le strategie dell'algoritmo di cui avrai bisogno.

Questo è un posto eccellente per utilizzare un'interfaccia.Abbiamo utilizzato il modello Strategia, che indica chiaramente un punto del progetto che cambierà.Pertanto, dovresti definire la strategia come un'interfaccia.In genere dovresti favorire le interfacce rispetto all'ereditarietà quando vuoi che un oggetto abbia un certo tipo;in questo caso, MediaStrategy.Affidarsi all'ereditarietà per l'identità del tipo è pericoloso;ti blocca in una particolare gerarchia di ereditarietà.Java non consente l'ereditarietà multipla, quindi non puoi estendere qualcosa che ti dia un'implementazione utile o più identità di tipo.

Tieni inoltre presente di non farti travolgere in OO (vedi blog) e modellare sempre gli oggetti in base al comportamento richiesto, se stavi progettando un'app in cui l'unico comportamento richiesto era un nome generico e una specie per un animale, allora avresti bisogno solo di una classe Animale con una proprietà per il nome, invece di milioni di classi per ogni possibile animale del mondo.

Ho una regola empirica approssimativa

Funzionalità: probabilmente sarà diverso in tutte le parti:Interfaccia.

Dati e funzionalità, le parti saranno per lo più le stesse, le parti diverse: classe astratta.

Dati e funzionalità effettivamente funzionanti, se estesi solo con lievi modifiche: classe ordinaria (concreta).

Dati e funzionalità, nessuna modifica prevista: classe ordinaria (concreta) con modificatore finale.

Dati e forse funzionalità:sola lettura: membri dell'enumerazione.

Questo è molto approssimativo e pronto e per nulla definito in modo rigoroso, ma esiste uno spettro che va dalle interfacce in cui tutto è destinato a essere modificato alle enumerazioni in cui tutto è fisso un po' come un file di sola lettura.

Le interfacce dovrebbero essere piccole.Davvero piccolo.Se stai davvero suddividendo i tuoi oggetti, le tue interfacce probabilmente conterranno solo alcuni metodi e proprietà molto specifici.

Le classi astratte sono scorciatoie.Ci sono cose condivise da tutti i derivati ​​​​di PetBase che puoi codificare una volta e finire?Se sì, allora è il momento di una lezione astratta.

Anche le classi astratte sono limitanti.Sebbene forniscano un'ottima scorciatoia per produrre oggetti figlio, qualsiasi oggetto può implementare solo una classe astratta.Molte volte trovo che questo sia un limite delle classi Abstract, ed è per questo che utilizzo molte interfacce.

Le classi astratte possono contenere diverse interfacce.La tua classe astratta PetBase può implementare IPet (gli animali domestici hanno proprietari) e IDigestion (gli animali domestici mangiano, o almeno dovrebbero).Tuttavia, PetBase probabilmente non implementerà IMammal, poiché non tutti gli animali domestici sono mammiferi e non tutti i mammiferi sono animali domestici.Puoi aggiungere un MammalPetBase che estende PetBase e aggiungere IMammal.FishBase potrebbe avere PetBase e aggiungere IFish.IFish avrebbe ISwim e IUnderwaterBreather come interfacce.

Sì, il mio esempio è eccessivamente complicato per l'esempio semplice, ma questo fa parte del bello di come le interfacce e le classi astratte lavorano insieme.

Il caso delle classi base rispetto alle interfacce è stato spiegato bene nelle linee guida per la codifica di Submain .NET:

Classi base vs.Interfacce Un tipo di interfaccia è una descrizione parziale di un valore, potenzialmente supportata da molti tipi di oggetti.Usa le classi di base anziché le interfacce quando possibile.Da un controllo delle versioni prospettiva, le classi sono più flessibili rispetto alle interfacce.Con una classe, puoi nave Versione 1.0 e poi in Versione 2.0 Aggiungere un nuovo metodo alla classe.Finché il metodo non è astratto, Tutte le classi derivate esistenti continuano per funzionare invariato.

Poiché le interfacce non supportano l'eredità dell'implementazione, il modello che si applica alle classi non si applica alle interfacce.L'aggiunta di un metodo a un'interfaccia è equivalente all'aggiunta di un metodo astratto a una classe di base;Qualsiasi classe che implementa l'interfaccia si interromperà perché la classe non implementa il nuovo metodo.Le interfacce sono appropriate nelle seguenti situazioni:

  1. Diverse classi non correlate desiderano supportare il protocollo.
  2. Queste classi hanno già stabilito classi di base (ad esempio alcune sono controlli di interfaccia utente (UI) e alcune sono servizi Web XML).
  3. L'aggregazione non è appropriata né praticabile.In tutte le altre situazioni, l'eredità di classe è un modello migliore.

Fonte: http://jasonroell.com/2014/12/09/interfaces-vs-abstract-classes-what-should-you-use/

C# è un linguaggio meraviglioso che è maturato e si è evoluto negli ultimi 14 anni.Questo è fantastico per noi sviluppatori perché un linguaggio maturo ci fornisce una miriade di funzionalità linguistiche a nostra disposizione.

Tuttavia, con molto potere si ottengono molte responsabilità.Alcune di queste funzionalità possono essere utilizzate in modo improprio o talvolta è difficile capire perché si sceglie di utilizzare una funzionalità rispetto a un'altra.Nel corso degli anni, una caratteristica con cui ho visto molti sviluppatori lottare è quando scegliere di utilizzare un'interfaccia o scegliere di utilizzare una classe astratta.Entrambi hanno vantaggi e svantaggi e il momento e il luogo corretti per utilizzarli.Ma come decidere???

Entrambi prevedono il riutilizzo di funzionalità comuni tra i tipi.La differenza più evidente è che le interfacce non forniscono alcuna implementazione per le loro funzionalità mentre le classi astratte consentono di implementare alcuni comportamenti "base" o "predefiniti" e quindi avere la possibilità di "sostituire" questo comportamento predefinito con i tipi derivati ​​dalle classi, se necessario .

Tutto questo va bene e consente un ottimo riutilizzo del codice e aderisce al principio DRY (Don't Repeat Yourself) dello sviluppo del software.Le classi astratte sono ottime da utilizzare quando si ha una relazione “è a”.

Per esempio:Un golden retriever “è un” tipo di cane.Così è un barboncino.Entrambi possono abbaiare, come tutti i cani.Tuttavia, potresti voler affermare che il parco dei barboncini è significativamente diverso dall'abbaiare del cane "predefinito".Pertanto, potrebbe avere senso implementare qualcosa come segue:

public abstract class Dog
{
      public virtual void Bark()
      {
        Console.WriteLine("Base Class implementation of Bark");
      }
}

public class GoldenRetriever : Dog
{
   // the Bark method is inherited from the Dog class
}

public class Poodle : Dog
{
  // here we are overriding the base functionality of Bark with our new implementation
  // specific to the Poodle class
  public override void Bark()
  {
     Console.WriteLine("Poodle's implementation of Bark");
  }
}

// Add a list of dogs to a collection and call the bark method.

void Main()
{
    var poodle = new Poodle();
    var goldenRetriever = new GoldenRetriever();

    var dogs = new List<Dog>();
    dogs.Add(poodle);
    dogs.Add(goldenRetriever);

    foreach (var dog in dogs)
    {
       dog.Bark();
    }
}

// Output will be:
// Poodle's implementation of Bark
// Base Class implementation of Bark

// 

Come puoi vedere, questo sarebbe un ottimo modo per mantenere il tuo codice DRY e consentire la chiamata dell'implementazione della classe base quando uno qualsiasi dei tipi può semplicemente fare affidamento sul Bark predefinito anziché su un'implementazione del caso speciale.Le classi come GoldenRetriever, Boxer, Lab potrebbero tutte ereditare la Bark "predefinita" (classe basso) gratuitamente solo perché implementano la classe astratta Dog.

Ma sono sicuro che lo sapevi già.

Sei qui perché vuoi capire perché potresti voler scegliere un'interfaccia invece di una classe astratta o viceversa.Bene, uno dei motivi per cui potresti voler scegliere un'interfaccia rispetto a una classe astratta è quando non hai o non vuoi impedire un'implementazione predefinita.Ciò è solitamente dovuto al fatto che i tipi che implementano l'interfaccia non sono correlati in una relazione "is a".In realtà, non devono essere affatto correlati, tranne per il fatto che ogni tipo “è in grado” o ha “l’abilità” di fare qualcosa o avere qualcosa.

Ora, cosa diavolo significa?Bene, ad esempio:Un essere umano non è un’anatra…e un’anatra non è un essere umano.Abbastanza ovvio.Tuttavia, sia un'anatra che un essere umano hanno "la capacità" di nuotare (dato che l'umano ha superato le lezioni di nuoto in prima elementare :)).Inoltre, poiché un'anatra non è un essere umano o viceversa, questa non è una relazione "è un", ma piuttosto una relazione "è capace" e possiamo usare un'interfaccia per illustrarlo:

// Create ISwimable interface
public interface ISwimable
{
      public void Swim();
}

// Have Human implement ISwimable Interface
public class Human : ISwimable

     public void Swim()
     {
        //Human's implementation of Swim
        Console.WriteLine("I'm a human swimming!");
     }

// Have Duck implement ISwimable interface
public class Duck: ISwimable
{
     public void Swim()
     {
          // Duck's implementation of Swim
          Console.WriteLine("Quack! Quack! I'm a Duck swimming!")
     }
}

//Now they can both be used in places where you just need an object that has the ability "to swim"

public void ShowHowYouSwim(ISwimable somethingThatCanSwim)
{
     somethingThatCanSwim.Swim();
}

public void Main()
{
      var human = new Human();
      var duck = new Duck();

      var listOfThingsThatCanSwim = new List<ISwimable>();

      listOfThingsThatCanSwim.Add(duck);
      listOfThingsThatCanSwim.Add(human);

      foreach (var something in listOfThingsThatCanSwim)
      {
           ShowHowYouSwim(something);
      }
}

 // So at runtime the correct implementation of something.Swim() will be called
 // Output:
 // Quack! Quack! I'm a Duck swimming!
 // I'm a human swimming!

L'uso di interfacce come il codice sopra ti consentirà di passare un oggetto a un metodo che "è in grado" di fare qualcosa.Al codice non interessa come lo fa... Tutto quello che sa è che può chiamare il metodo Swim su quell'oggetto e quell'oggetto saprà quale comportamento assumere in fase di esecuzione in base al suo tipo.

Ancora una volta, questo aiuta il tuo codice a rimanere DRY in modo da non dover scrivere più metodi che chiamano l'oggetto per eseguire la stessa funzione principale (ShowHowHumanSwims(human), ShowHowDuckSwims(duck), ecc.)

L'utilizzo di un'interfaccia qui consente ai metodi chiamanti di non doversi preoccupare di quale tipo è o di come viene implementato il comportamento.Sa solo che, data l'interfaccia, ogni oggetto dovrà aver implementato il metodo Swim, quindi è sicuro chiamarlo nel proprio codice e consentire che il comportamento del metodo Swim venga gestito all'interno della propria classe.

Riepilogo:

Quindi la mia regola pratica principale è utilizzare una classe astratta quando si desidera implementare una funzionalità "predefinita" per una gerarchia di classi e/o e le classi o i tipi con cui si lavora condividono una relazione "è un" (es.il barboncino “è un” tipo di cane).

D'altra parte usa un'interfaccia quando non hai una relazione "è a" ma hai tipi che condividono "la capacità" di fare qualcosa o avere qualcosa (es.L'anatra "non è" un essere umano.Tuttavia, l’anatra e l’uomo condividono “la capacità” di nuotare).

Un'altra differenza da notare tra classi astratte e interfacce è che una classe può implementare da una a molte interfacce ma una classe può ereditare solo da UNA classe astratta (o da qualsiasi classe per quella materia).Sì, puoi nidificare le classi e avere una gerarchia di ereditarietà (cosa che molti programmi fanno e dovrebbero avere) ma non puoi ereditare due classi in una definizione di classe derivata (questa regola si applica a C#.In alcune altre lingue è possibile farlo, di solito solo a causa della mancanza di interfacce in queste lingue).

Quando si utilizzano le interfacce, ricordare inoltre di aderire al principio di segregazione delle interfacce (ISP).L'ISP afferma che nessun cliente dovrebbe essere costretto a dipendere da metodi che non utilizza.Per questo motivo le interfacce dovrebbero essere focalizzate su compiti specifici e solitamente sono molto piccole (es.IDisposable, IComparable ).

Un altro suggerimento è che se stai sviluppando funzionalità piccole e concise, utilizza le interfacce.Se stai progettando unità funzionali di grandi dimensioni, utilizza una classe astratta.

Spero che questo chiarisca le cose per alcune persone!

Inoltre, se ti vengono in mente esempi migliori o vuoi sottolineare qualcosa, fallo nei commenti qui sotto!

Una differenza importante è che puoi solo ereditare uno classe base, ma puoi implementare molti interfacce.Quindi vuoi usare una classe base solo se lo sei assolutamente certo che non sarà necessario ereditare anche una classe base diversa.Inoltre, se ritieni che la tua interfaccia stia diventando grande, dovresti iniziare a suddividerla in alcune parti logiche che definiscono funzionalità indipendenti, poiché non esiste una regola secondo cui la tua classe non può implementarle tutte (o che puoi definire una diversa interfaccia che li eredita tutti per raggrupparli).

Quando ho iniziato a conoscere la programmazione orientata agli oggetti, ho commesso l'errore facile e probabilmente comune di utilizzare l'ereditarietà per condividere un comportamento comune, anche laddove tale comportamento non era essenziale per la natura dell'oggetto.

Per basarci ulteriormente su un esempio molto utilizzato in questa particolare questione, ci sono molti di cose petabili: fidanzate, automobili, coperte pelose...- quindi avrei potuto avere una classe Petable che forniva questo comportamento comune e varie classi che ne ereditavano.

Tuttavia, essere petabili non fa parte della natura di nessuno di questi oggetti.Ci sono concetti molto più importanti che Sono essenziali per la loro natura: la fidanzata è una persona, l'auto è un veicolo terrestre, il gatto è un mammifero...

I comportamenti dovrebbero essere assegnati prima alle interfacce (inclusa l'interfaccia predefinita della classe) e promossi a classe base solo se sono (a) comuni a un ampio gruppo di classi che sono sottoinsiemi di una classe più ampia - nello stesso senso che "gatto" e "persona" sono sottoinsiemi di "mammifero".

Il problema è che, dopo aver compreso la progettazione orientata agli oggetti sufficientemente meglio di quanto avessi capito io all'inizio, normalmente lo farai automaticamente senza nemmeno pensarci.Quindi la nuda verità dell'affermazione "codice per un'interfaccia, non una classe astratta" diventa così ovvia che fai fatica a credere che qualcuno si preoccuperebbe di dirlo - e inizi a provare a leggervi altri significati.

Un'altra cosa che aggiungerei è che se una classe lo è puramente abstract - senza membri o metodi non astratti, non ereditati esposti a figlio, genitore o client - allora perché è una classe?Potrebbe essere sostituito, in alcuni casi da un'interfaccia e in altri casi da Null.

Preferire le interfacce alle classi astratte

Razionale I punti principali da considerare [due già menzionati qui] sono:

  • Le interfacce sono più flessibili, perché una classe può implementare più Interfacce.Poiché Java non ha ereditarietà multipla, l'utilizzo di Le classi astratte impediscono agli utenti di utilizzare qualsiasi altra classe gerarchia. In generale, preferire le interfacce quando non ci sono interfacce predefinite implementazioni o stato. Le collezioni Java offrono buoni esempi di this (Mappa, Set, ecc.).
  • Le classi astratte hanno il vantaggio di consentire un migliore inoltro compatibilità.Una volta che i client utilizzano un'interfaccia, non è possibile modificarla;Se usano una classe astratta, è comunque possibile aggiungere il comportamento senza Interruzione del codice esistente. Se la compatibilità è un problema, prendere in considerazione l'utilizzo classi astratte.
  • Anche se disponi di implementazioni predefinite o di uno stato interno,considerare l'offerta di un'interfaccia e di una sua implementazione astratta.Questo aiuterà i clienti, ma consentirà loro comunque una maggiore libertà se desiderato [1].
    Naturalmente, l'argomento è stato discusso a lungo altrove [2,3].

[1] Aggiunge più codice, ovviamente, ma se la brevità è la tua preoccupazione principale, probabilmente avresti dovuto evitare Java in primo luogo!

[2] Joshua Bloch, Effective Java, punti 16-18.

[3] http://www.codeproject.com/KB/ar...

I commenti precedenti sull'utilizzo di classi astratte per l'implementazione comune sono decisamente nel segno.Un vantaggio che non ho ancora visto menzionato è che l'uso delle interfacce rende molto più semplice l'implementazione di oggetti fittizi ai fini dei test unitari.La definizione di IPet e PetBase come descritto da Jason Cohen ti consente di simulare facilmente diverse condizioni di dati, senza il sovraccarico di un database fisico (finché non decidi che è il momento di testare la cosa reale).

Non utilizzare una classe base a meno che tu non sappia cosa significa e che si applica in questo caso.Se applicabile, usalo, altrimenti usa le interfacce.Ma nota la risposta sulle piccole interfacce.

L'ereditarietà pubblica è abusata in OOD ed esprime molto di più di quanto la maggior parte degli sviluppatori si renda conto o sia disposta a essere all'altezza.Vedi il Principio di sostituibilità di Liskov

In breve, se A "è un" B allora A non richiede niente di più di B e non fornisce niente di meno di B, per ogni metodo che espone.

Concettualmente, un'interfaccia viene utilizzata per definire formalmente e semi-formalmente un insieme di metodi che un oggetto fornirà.Formalmente significa un insieme di nomi e firme di metodi, semi-formalmente significa documentazione leggibile dall'uomo associata a tali metodi.Le interfacce sono solo descrizioni di un'API (dopo tutto, API sta per Application Programmer Interfaccia), non possono contenere alcuna implementazione e non è possibile utilizzare o eseguire un'interfaccia.Rendono esplicito solo il contratto su come dovresti interagire con un oggetto.

Le classi forniscono un'implementazione, possono dichiarare di implementare zero, una o più interfacce.Se si intende ereditare una classe, la convenzione è quella di anteporre al nome della classe "Base".

Esiste una distinzione tra una classe base e una classe base astratta (ABC).Gli ABC mescolano insieme interfaccia e implementazione.Astratto al di fuori della programmazione informatica significa "riassunto", cioè "Astratto == Interfaccia".Una classe base astratta può quindi descrivere sia un'interfaccia, sia un'implementazione vuota, parziale o completa che si intende ereditare.

Le opinioni su quando utilizzare le interfacce rispetto alle classi base astratte rispetto alle sole classi varieranno notevolmente in base sia a ciò che stai sviluppando sia al linguaggio in cui stai sviluppando.Le interfacce sono spesso associate solo a linguaggi tipizzati staticamente come Java o C#, ma i linguaggi tipizzati dinamicamente possono anche avere interfacce e classi base astratte.In Python, ad esempio, la distinzione è chiara tra una Classe, che dichiara che it implementa un'interfaccia e un oggetto, che è un'istanza di una classe, e si dice che sia fornire quell'interfaccia.È possibile in un linguaggio dinamico che due oggetti che sono entrambe istanze della stessa Classe, possano dichiarare di fornire completamente diverso interfacce.In Python questo è possibile solo per gli attributi degli oggetti, mentre i metodi sono uno stato condiviso tra tutti gli oggetti di una Classe.Tuttavia in Ruby, gli oggetti possono avere metodi per istanza, quindi è possibile che l'interfaccia tra due oggetti della stessa classe possa variare quanto desidera il programmatore (tuttavia, Ruby non ha alcun modo esplicito di dichiarare le interfacce).

Nei linguaggi dinamici l'interfaccia per un oggetto viene spesso presupposta implicitamente, sia eseguendo l'introspezione di un oggetto e chiedendogli quali metodi fornisce (Look Before You Leap) o preferibilmente semplicemente tentando di utilizzare l'interfaccia desiderata su un oggetto e rilevando eccezioni se l'oggetto non fornisce quell'interfaccia (è più facile chiedere perdono che autorizzazione).Ciò può portare a "falsi positivi" in cui due interfacce hanno lo stesso nome di metodo ma sono semanticamente diverse, tuttavia il compromesso è che il codice è più flessibile poiché non è necessario specificare eccessivamente in anticipo per anticipare tutti i possibili usi del tuo codice

Un'altra opzione da tenere a mente è l'utilizzo della relazione "ha-a", ovvero "è implementata in termini di" o "composizione". A volte questo è un modo più pulito e flessibile per strutturare le cose rispetto all'uso dell'ereditarietà "è-a".

Potrebbe non avere molto senso dal punto di vista logico affermare che cane e gatto "hanno" entrambi un animale domestico, ma evita le trappole comuni dell'ereditarietà multipla:

public class Pet
{
    void Bathe();
    void Train(Trick t);
}

public class Dog
{
    private Pet pet;

    public void Bathe() { pet.Bathe(); }
    public void Train(Trick t) { pet.Train(t); }
}

public class Cat
{
    private Pet pet;

    public void Bathe() { pet.Bathe(); }
    public void Train(Trick t) { pet.Train(t); }
}

Sì, questo esempio mostra che nel fare le cose in questo modo c'è molta duplicazione del codice e mancanza di eleganza.Ma si dovrebbe anche apprezzare che questo aiuta a mantenere Cane e Gatto disaccoppiati dalla classe Animale Domestico (in quanto Cane e Gatto non hanno accesso ai membri privati ​​di Animale Domestico), e lascia spazio a Cane e Gatto per ereditare da qualcos'altro- -forse la classe dei mammiferi.

La composizione è preferibile quando non è richiesto l'accesso privato e non è necessario fare riferimento a Cane e Gatto utilizzando riferimenti/indicatori di animali domestici generici.Le interfacce ti danno quella capacità di riferimento generica e possono aiutarti a ridurre la verbosità del tuo codice, ma possono anche offuscare le cose quando sono mal organizzate.L'ereditarietà è utile quando hai bisogno dell'accesso come membro privato e, utilizzandola, ti impegni ad accoppiare fortemente le tue classi Cane e Gatto con la tua classe Animale domestico, il che è un costo elevato da pagare.

Tra ereditarietà, composizione e interfacce non esiste un modo che sia sempre corretto ed è utile considerare come tutte e tre le opzioni possano essere utilizzate in armonia.Delle tre, l'ereditarietà è in genere l'opzione da utilizzare meno spesso.

Dipende dalle tue esigenze.Se IPet è abbastanza semplice, preferirei implementarlo.Altrimenti, se PetBase implementa un sacco di funzionalità che non vuoi duplicare, allora fallo.

Lo svantaggio dell'implementazione di una classe base è l'obbligo di farlo override (O new) metodi esistenti.Questo li rende metodi virtuali, il che significa che devi stare attento a come usi l'istanza dell'oggetto.

Infine, la singola eredità di .NET mi uccide.Un esempio ingenuo:Supponiamo che tu stia creando un controllo utente, quindi erediti UserControl.Ma ora non puoi nemmeno ereditare PetBase.Questo ti costringe a riorganizzarti, ad esempio a creare a PetBase membro della classe, invece.

@Gioele:Alcuni linguaggi (ad esempio C++) consentono l'ereditarietà multipla.

Di solito non lo implemento finché non ne ho bisogno.Prediligo le interfacce rispetto alle classi astratte perché offrono un po' più di flessibilità.Se c'è un comportamento comune in alcune delle classi ereditarie, lo sposto e creo una classe base astratta.Non vedo la necessità di entrambi, dal momento che essenzialmente servono allo stesso scopo, e averli entrambi è un cattivo odore di codice (imho) che la soluzione è stata sovraingegnerizzata.

Per quanto riguarda C#, in un certo senso le interfacce e le classi astratte possono essere intercambiabili.Tuttavia, le differenze sono:i) le interfacce non possono implementare il codice;ii) per questo motivo, le interfacce non possono richiamare più in alto nello stack alla sottoclasse;e iii) solo la classe astratta può essere ereditata su una classe, mentre su una classe possono essere implementate più interfacce.

Per definizione, l'interfaccia fornisce un livello per comunicare con altro codice.Tutte le proprietà e i metodi pubblici di una classe implementano per impostazione predefinita l'interfaccia implicita.Possiamo anche definire un'interfaccia come un ruolo, ogni volta che una classe ha bisogno di svolgere quel ruolo, deve implementarlo dandogli diverse forme di implementazione a seconda della classe che lo implementa.Quindi quando parli di interfaccia, parli di polimorfismo e quando parli di classe base, parli di ereditarietà.Due concetti di ops!!!

Ho scoperto che un modello di Interfaccia > Abstract > Concrete funziona nel seguente caso d'uso:

1.  You have a general interface (eg IPet)
2.  You have a implementation that is less general (eg Mammal)
3.  You have many concrete members (eg Cat, Dog, Ape)

La classe astratta definisce gli attributi condivisi predefiniti delle classi concrete, ma impone l'interfaccia.Per esempio:

public interface IPet{

    public boolean hasHair();

    public boolean walksUprights();

    public boolean hasNipples();
}

Ora, poiché tutti i mammiferi hanno capelli e capezzoli (per quanto ne sappia, non sono uno zoologo), possiamo inserirli nella classe base astratta

public abstract class Mammal() implements IPet{

     @override
     public walksUpright(){
         throw new NotSupportedException("Walks Upright not implemented");
     }

     @override
     public hasNipples(){return true}

     @override
     public hasHair(){return true}

E poi le classi concrete definiscono semplicemente che camminano eretti.

public class Ape extends Mammal(){

    @override
    public walksUpright(return true)
}

public class Catextends Mammal(){

    @override
    public walksUpright(return false)
}

Questo design è interessante quando ci sono molte classi concrete e non vuoi mantenere il boilerplate solo per programmare su un'interfaccia.Se venissero aggiunti nuovi metodi all'interfaccia, tutte le classi risultanti verrebbero interrotte, quindi otterrai comunque i vantaggi dell'approccio dell'interfaccia.

In questo caso l’astratto potrebbe anche essere concreto;tuttavia, la designazione astratta aiuta a sottolineare che viene utilizzato questo modello.

Un erede di una classe base dovrebbe avere una relazione "is a".L'interfaccia rappresenta una relazione "implementa una".Quindi usa una classe base solo quando i tuoi eredi manterranno la relazione.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top