Cosa significa “programma per interfacce, non implementazioni”?

Ci si imbatte in questa frase quando si leggono modelli di design.

Ma non lo capisco, qualcuno potrebbe spiegarlo per me?

Le interfacce sono solo contratti o firme e non sanno nulla delle implementazioni.

Coding contro mezzi di interfaccia, il codice client contiene sempre un object Interface che viene fornito da una fabbrica. Qualsiasi istanza restituita dalla fabbrica sarebbe di tipo Interfaccia che deve essere implementata da qualsiasi class candidato in fabbrica. In questo modo il programma client non è preoccupato per l’implementazione e la firma dell’interfaccia determina ciò che tutte le operazioni possono essere eseguite. Questo può essere usato per modificare il comportamento di un programma in fase di esecuzione. Ti aiuta anche a scrivere programmi migliori dal punto di vista della manutenzione.

Ecco un esempio di base per te.

public enum Language { English, German, Spanish } public class SpeakerFactory { public static ISpeaker CreateSpeaker(Language language) { switch (language) { case Language.English: return new EnglishSpeaker(); case Language.German: return new GermanSpeaker(); case Language.Spanish: return new SpanishSpeaker(); default: throw new ApplicationException("No speaker can speak such language"); } } } [STAThread] static void Main() { //This is your client code. ISpeaker speaker = SpeakerFactory.CreateSpeaker(Language.English); speaker.Speak(); Console.ReadLine(); } public interface ISpeaker { void Speak(); } public class EnglishSpeaker : ISpeaker { public EnglishSpeaker() { } #region ISpeaker Members public void Speak() { Console.WriteLine("I speak English."); } #endregion } public class GermanSpeaker : ISpeaker { public GermanSpeaker() { } #region ISpeaker Members public void Speak() { Console.WriteLine("I speak German."); } #endregion } public class SpanishSpeaker : ISpeaker { public SpanishSpeaker() { } #region ISpeaker Members public void Speak() { Console.WriteLine("I speak Spanish."); } #endregion } 

alt text http://ruchitsurati.net/myfiles/interface.png

Questo è solo un esempio di base e la spiegazione effettiva del principio va oltre lo scopo di questa risposta.

MODIFICARE

Ho aggiornato l’esempio sopra e ho aggiunto una class base Speaker astratta. In questo aggiornamento, ho aggiunto una funzione a tutti gli Spakers a “SayHello”. Tutti i relatori parlano “Hello World”. Quindi questa è una caratteristica comune con funzioni simili. Fai riferimento allo schema delle classi e troverai che la class abstract Speaker implementa l’interfaccia ISpeaker e contrassegna il Speak () come astratto, il che significa che l’implementazione di ogni Speaker è responsabile dell’attuazione del metodo Speak poiché varia da Speaker a Speaker. Ma tutti gli oratori dicono “Ciao” all’unanimità. Quindi nella class Speaker astratta definiamo un metodo che dice “Hello World” e ogni implementazione di Speaker deriverà dal metodo SayHello.

Considera un caso in cui SpanishSpeaker non può dire Say Hello, quindi in questo caso puoi sovrascrivere il metodo SayHello per Spanish Speaker e sollevare un’eccezione appropriata.

Si prega di notare che, non abbiamo apportato alcuna modifica all’interfaccia ISpeaker. E il codice client e SpeakerFactory rimangono inalterati inalterati. E questo è ciò che realizziamo mediante la programmazione sull’interfaccia .

E potremmo ottenere questo comportamento semplicemente aggiungendo un altoparlante di class astratta di base e alcune modifiche minori in Ogni implementazione lasciando il programma originale invariato. Questa è una caratteristica desiderata di qualsiasi applicazione e rende la tua applicazione facilmente mantenibile.

 public enum Language { English, German, Spanish } public class SpeakerFactory { public static ISpeaker CreateSpeaker(Language language) { switch (language) { case Language.English: return new EnglishSpeaker(); case Language.German: return new GermanSpeaker(); case Language.Spanish: return new SpanishSpeaker(); default: throw new ApplicationException("No speaker can speak such language"); } } } class Program { [STAThread] static void Main() { //This is your client code. ISpeaker speaker = SpeakerFactory.CreateSpeaker(Language.English); speaker.Speak(); Console.ReadLine(); } } public interface ISpeaker { void Speak(); } public abstract class Speaker : ISpeaker { #region ISpeaker Members public abstract void Speak(); public virtual void SayHello() { Console.WriteLine("Hello world."); } #endregion } public class EnglishSpeaker : Speaker { public EnglishSpeaker() { } #region ISpeaker Members public override void Speak() { this.SayHello(); Console.WriteLine("I speak English."); } #endregion } public class GermanSpeaker : Speaker { public GermanSpeaker() { } #region ISpeaker Members public override void Speak() { Console.WriteLine("I speak German."); this.SayHello(); } #endregion } public class SpanishSpeaker : Speaker { public SpanishSpeaker() { } #region ISpeaker Members public override void Speak() { Console.WriteLine("I speak Spanish."); } public override void SayHello() { throw new ApplicationException("I cannot say Hello World."); } #endregion } 

alt text http://demo.ruchitsurati.net/myfiles/interface1.png

Pensa a un’interfaccia come un contratto tra un object e i suoi clienti. Questa è l’interfaccia specifica le cose che un object può fare e le firme per accedere a quelle cose.

Le implementazioni sono i comportamenti effettivi. Ad esempio, hai un metodo sort (). È ansible implementare QuickSort o MergeSort. Ciò non dovrebbe avere importanza per il codice client che chiama l’ordinamento finché l’interfaccia non cambia.

Librerie come Java API e .NET Framework fanno un uso pesante delle interfacce perché milioni di programmatori utilizzano gli oggetti forniti. I creatori di queste librerie devono stare molto attenti a non cambiare l’interfaccia con le classi in queste librerie perché influenzeranno tutti i programmatori che usano la libreria. D’altra parte possono cambiare l’implementazione quanto vogliono.

Se, come programmatore, si codifica contro l’implementazione, non appena si modifica il codice smette di funzionare. Quindi pensa ai vantaggi dell’interfaccia in questo modo:

  1. nasconde le cose che non è necessario sapere rendendo l’object più semplice da usare.
  2. fornisce il contratto su come si comporterà l’object in modo che tu possa dipendere da quello

Significa che dovresti provare a scrivere il tuo codice in modo che utilizzi direttamente un’astrazione (class astratta o interfaccia) anziché l’implementazione.

Normalmente l’implementazione viene iniettata nel codice tramite il costruttore o una chiamata al metodo. Quindi, il tuo codice conosce l’interfaccia o la class astratta e può chiamare qualsiasi cosa definita in questo contratto. Come object reale (implementazione dell’interfaccia / class astratta), le chiamate funzionano sull’object.

Questo è un sottoinsieme del Liskov Substitution Principle (LSP), la L dei principi SOLID .

Un esempio in .NET sarebbe quello di codificare con IList anziché List o Dictionary , quindi è ansible utilizzare qualsiasi class che implementa IList intercambiabile nel codice:

 // myList can be _any_ object that implements IList public int GetListCount(IList myList) { // Do anything that IList supports return myList.Count(); } 

Un altro esempio della Base Class Library (BCL) è la class astratta ProviderBase fornisce un’infrastruttura e, cosa importante, significa che tutte le implementazioni del provider possono essere utilizzate in modo intercambiabile se si codifica contro di essa.

Questa affermazione riguarda l’accoppiamento. Un potenziale motivo per utilizzare la programmazione orientata agli oggetti è il riutilizzo. Così, ad esempio, puoi dividere il tuo algoritmo tra due oggetti collaborativi A e B. Ciò potrebbe essere utile per la creazione successiva di un altro algoritmo, che potrebbe riutilizzare uno o l’altro dei due oggetti. Tuttavia, quando tali oggetti comunicano (inviano messaggi – metodi di chiamata), creano dipendenze tra loro. Ma se vuoi usarne uno senza l’altro, devi specificare cosa dovrebbe fare qualche altro object C per l’object A se sostituiamo B. Queste descrizioni sono chiamate interfacce. Ciò consente all’object A di comunicare senza modifiche con oggetti diversi che si basano sull’interfaccia. L’affermazione che hai citato dice che se hai intenzione di riutilizzare parte di un algoritmo (o più in generale un programma), dovresti creare interfacce e fare affidamento su di esse, così potresti cambiare l’implementazione concreta in qualsiasi momento senza cambiare altri oggetti se usi il interfaccia dichiarata.

Se dovessi scrivere una Classe di Car in un’era di Combustion-Car, allora c’è una grande possibilità che implementerai oilChange () come parte di questa class. Ma, quando vengono introdotte le auto elettriche, ti troverebbero nei guai perché non sono coinvolti i cambi di olio per queste auto e nessuna implementazione.

La soluzione al problema è di avere un’interfaccia performMaintenance () nella class Car e hide i dettagli all’interno dell’implementazione appropriata. Ogni tipo di automobile fornisce la propria implementazione per eseguire Maintenance (). Come proprietario di un’Auto, tutto ciò che devi affrontare è performMaintenance () e non preoccuparti dell’adattamento quando c’è un CHANGE.

 class MaintenanceSpecialist { public: virtual int performMaintenance() = 0; }; class CombustionEnginedMaintenance : public MaintenanceSpecialist { int performMaintenance() { printf("combustionEnginedMaintenance: We specialize in maintenance of Combustion engines \n"); return 0; } }; class ElectricMaintenance : public MaintenanceSpecialist { int performMaintenance() { printf("electricMaintenance: We specialize in maintenance of Electric Cars \n"); return 0; } }; class Car { public: MaintenanceSpecialist *mSpecialist; virtual int maintenance() { printf("Just wash the car \n"); return 0; }; }; class GasolineCar : public Car { public: GasolineCar() { mSpecialist = new CombustionEnginedMaintenance(); } int maintenance() { mSpecialist->performMaintenance(); return 0; } }; class ElectricCar : public Car { public: ElectricCar() { mSpecialist = new ElectricMaintenance(); } int maintenance(){ mSpecialist->performMaintenance(); return 0; } }; int _tmain(int argc, _TCHAR* argv[]) { Car *myCar; myCar = new GasolineCar(); myCar->maintenance(); /* I dont know what is involved in maintenance. But, I do know the maintenance has to be performsd */ myCar = new ElectricCar(); myCar->maintenance(); return 0; } 

Spiegazione aggiuntiva: sei un proprietario dell’auto che possiede più auto. Ritaglia il servizio che vuoi esternalizzare. Nel nostro caso vogliamo esternalizzare il lavoro di manutenzione di tutte le auto.

  1. Identifichi il contratto (Interface) che vale per tutte le tue auto e fornitori di servizi.
  2. I fornitori di servizi escono con un meccanismo per fornire il servizio.
  3. Non ti devi preoccupare di associare il tipo di auto al fornitore di servizi. Basta specificare quando si desidera pianificare la manutenzione e invocarla. Un’azienda di servizi appropriata dovrebbe intervenire ed eseguire i lavori di manutenzione.

    Approccio alternativo

  4. Identifica il lavoro (può essere una nuova interfaccia dell’interfaccia) che vale per tutte le tue auto.
  5. Esci con un meccanismo per fornire il servizio. Fondamentalmente fornirai l’implementazione.
  6. Invochi il lavoro e fallo da solo. Qui svolgerai il lavoro di manutenzione appropriata.

    Qual è il lato negativo del secondo approccio? Potresti non essere l’esperto nel trovare il modo migliore per eseguire la manutenzione. Il tuo compito è guidare la macchina e divertirti. Non essere nel business di mantenerlo.

    Qual è il lato negativo del primo approccio? C’è il sovraccarico di trovare un’azienda ecc. A meno che tu non sia un’azienda di noleggio auto, potrebbe non valerne la pena.

Come altri hanno già detto, significa che il tuo codice chiamante dovrebbe conoscere solo un genitore astratto, NON la class di implementazione effettiva che farà il lavoro.

Ciò che aiuta a capire questo è il PERCHE ‘si dovrebbe sempre programmare su un’interfaccia. Ci sono molte ragioni, ma due delle più facili da spiegare sono

1) Test.

Diciamo che ho il mio intero codice di database in una class. Se il mio programma conosce la class concreta, posso solo testare il mio codice eseguendolo davvero contro quella class. Sto usando -> per significare “parla con”.

WorkerClass -> DALClass Tuttavia, aggiungiamo un’interfaccia al mix.

WorkerClass -> IDAL -> DALClass.

Quindi DALClass implementa l’interfaccia IDAL e la class worker lo chiama SOLTANTO.

Ora se vogliamo scrivere test per il codice, potremmo invece creare una semplice class che si comporti come un database.

WorkerClass -> IDAL -> IFakeDAL.

2) Riutilizzo

Seguendo l’esempio sopra, diciamo che vogliamo passare da SQL Server (che utilizza il nostro concreto DALClass) a MonogoDB. Ciò richiederebbe un grande lavoro, ma NON se abbiamo programmato un’interfaccia. In questo caso scriviamo semplicemente la nuova class DB e cambiamo (tramite la fabbrica)

WorkerClass -> IDAL -> DALClass

a

WorkerClass -> IDAL -> MongoDBClass

le interfacce descrivono le capacità. quando scrivi un codice imperativo, parla delle capacità che stai utilizzando, piuttosto che tipi o classi specifici.