Che cosa significa "programma per interfacce, non implementazioni"?


Risposte:


148

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

La codifica in base all'interfaccia indica che il codice client contiene sempre un oggetto Interface fornito da una factory. Qualsiasi istanza restituita dalla fabbrica sarebbe di tipo Interface che qualsiasi classe candidata alla fabbrica deve aver implementato. In questo modo il programma client non è preoccupato per l'implementazione e la firma dell'interfaccia determina quali operazioni possono essere eseguite. Questo può essere usato per cambiare il comportamento di un programma in fase di esecuzione. Ti aiuta anche a scrivere programmi molto 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
}

testo alternativo

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

MODIFICARE

Ho aggiornato l'esempio sopra e ho aggiunto una Speakerclasse base astratta . In questo aggiornamento, ho aggiunto una funzione a tutti gli altoparlanti a "SayHello". Tutti gli oratori parlano "Hello World". Quindi questa è una caratteristica comune con una funzione simile. Fai riferimento al diagramma di classe e scoprirai che la Speakerclasse astratta implementa l' ISpeakerinterfaccia e contrassegna Speak()come astratta il che significa che ogni implementazione di Speaker è responsabile dell'implementazione del Speak()metodo poiché varia da Speakera Speaker. Ma tutti gli oratori dicono "Ciao" all'unanimità. Quindi nella classe Speaker astratta definiamo un metodo che dice "Hello World" e ogni Speakerimplementazione ne deriverà il SayHello()metodo.

Considera un caso in cui SpanishSpeakernon puoi dire Ciao, quindi in quel caso puoi ignorare il SayHello()metodo per lo spagnolo e sollevare un'eccezione adeguata.

Si noti che non abbiamo apportato modifiche a Interface ISpeaker. E anche il codice client e SpeakerFactory rimangono invariati. E questo è ciò che realizziamo programmando l'interfaccia .

E potremmo ottenere questo comportamento semplicemente aggiungendo un Speaker di classe astratto di base e alcune modifiche minori in ogni implementazione, lasciando così il programma originale invariato. Questa è una caratteristica desiderata di qualsiasi applicazione e la rende facilmente gestibile.

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
}

testo alternativo


19
La programmazione per l'interfaccia non riguarda solo il tipo di variabile di riferimento. Significa anche che non si utilizzano ipotesi implicite sulla propria implementazione. Ad esempio, se si utilizza un Listcome tipo, è comunque possibile supporre che l'accesso casuale sia veloce chiamando ripetutamente get(i).
Joachim Sauer,

16
Le fabbriche sono ortogonali alla programmazione delle interfacce, ma penso che questa spiegazione faccia sembrare che ne facciano parte.
T.

@Toon: sono d'accordo con te. Volevo fornire un esempio molto semplice e di base per la programmazione da interfaccia. Non volevo confondere l'interrogante implementando l'interfaccia IFlyable su alcune classi di uccelli e animali.
questo. __curious_geek

@Questo. se invece utilizzo una classe astratta o un modello di facciata, verrà comunque chiamato "programma per un'interfaccia"? o devo esplicitamente usare un'interfaccia e implementarla su una classe?
never_had_a_name

1
Quale strumento uml stavi usando per creare le immagini?
Adam Arold,

29

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

Le implementazioni sono i comportamenti reali. Supponiamo ad esempio di avere un metodo sort (). È possibile implementare QuickSort o MergeSort. Ciò non dovrebbe importare all'ordinamento del codice client, purché l'interfaccia non cambi.

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

Se, come programmatore, codifichi l'implementazione, non appena cambia il codice smette di funzionare. Quindi, pensa ai vantaggi dell'interfaccia in questo modo:

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

Significa che devi essere consapevole di ciò che stai contraendo l'oggetto da fare: nell'esempio a condizione che stai contraendo solo un ordinamento, non necessariamente un ordinamento stabile.
penguat,

Così simile a come la documentazione della biblioteca non menziona l'implementazione, sono solo descrizioni delle interfacce di classe incluse.
Joe Iddon,

17

Significa che dovresti provare a scrivere il tuo codice in modo che utilizzi direttamente un'astrazione (classe o interfaccia astratta) invece dell'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 classe astratta e può chiamare tutto ciò che è definito in questo contratto. Quando viene utilizzato un oggetto reale (implementazione dell'interfaccia / classe astratta), le chiamate stanno operando sull'oggetto.

Questo è un sottoinsieme di Liskov Substitution Principle(LSP), la L dei SOLIDprincipi.

Un esempio in .NET potrebbe essere la codifica IListinvece di Listo Dictionary, quindi è possibile utilizzare qualsiasi classe implementabile in modo IListintercambiabile 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 Biblioteca di classe di base (BCL) è la ProviderBaseclasse astratta: questo fornisce alcune infrastrutture e, cosa più importante, significa che tutte le implementazioni del provider possono essere utilizzate in modo intercambiabile se si codifica contro di essa.


ma come può un client interagire con un'interfaccia e usare i suoi metodi vuoti?
never_had_a_name

1
Il client non interagisce con l'interfaccia, ma attraverso l'interfaccia :) Gli oggetti interagiscono con altri oggetti attraverso metodi (messaggi) e un'interfaccia è una sorta di lingua - quando sai che un determinato oggetto (persona) implementa (parla) inglese (IList ), puoi usarlo senza bisogno di saperne di più su quell'oggetto (che è anche un italiano), perché non è necessario in quel contesto (se vuoi chiedere aiuto non devi sapere che parla anche italiano se capisci l'inglese).
Gabriel Ščerbák,

BTW. Il principio di sostituzione IMHO di Liskov riguarda la semantica dell'eredità e non ha nulla a che fare con le interfacce, che possono essere trovate anche in lingue senza eredità (Vai da Google).
Gabriel Ščerbák,

5

Se dovessi scrivere una Car Class nell'era Combustion-Car, allora c'è una grande possibilità di implementare oilChange () come parte di questa classe. Ma quando vengono introdotte le auto elettriche, si sarebbe nei guai in quanto non vi è alcun cambio d'olio per queste auto e nessuna implementazione.

La soluzione al problema è avere un'interfaccia performMaintenance () nella classe Car e nascondere i dettagli all'interno dell'implementazione appropriata. Ogni tipo di automobile fornirebbe la propria implementazione per performMaintenance (). Come proprietario di un'auto, tutto ciò che devi fare è eseguireManutenzione () e non preoccuparti di adattarti in caso di CAMBIAMENTO.

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 performed */


    myCar = new ElectricCar(); 
    myCar->maintenance(); 

    return 0;
}

Spiegazione aggiuntiva: sei un proprietario di auto che possiede più auto. Ritagli il servizio che desideri esternalizzare. Nel nostro caso vogliamo esternalizzare i lavori di manutenzione di tutte le auto.

  1. Identifichi il contratto (interfaccia) valido per tutte le auto e i fornitori di servizi.
  2. I fornitori di servizi escono con un meccanismo per fornire il servizio.
  3. Non devi preoccuparti di associare il tipo di auto al fornitore di servizi. Devi solo specificare quando vuoi programmare la manutenzione e invocarla. Una società di servizi appropriata dovrebbe intervenire ed eseguire i lavori di manutenzione.

    Approccio alternativo

  4. Identifichi il lavoro (può essere una nuova interfaccia) che vale per tutte le tue auto.
  5. Si esce con un meccanismo per fornire il servizio. Fondamentalmente hai intenzione di fornire l'implementazione.
  6. Invoca il lavoro e lo fai da solo. Qui eseguirai il lavoro di manutenzione appropriata.

    Qual è il rovescio della medaglia del secondo approccio? Potresti non essere l'esperto nel trovare il modo migliore per eseguire la manutenzione. Il tuo compito è guidare la macchina e godertela. Non essere nel business di mantenerlo.

    Qual è il rovescio della medaglia del primo approccio? C'è il sovraccarico di trovare una compagnia ecc. A meno che tu non sia una compagnia di autonoleggio, potrebbe non valere la pena.


4

Questa affermazione riguarda l'accoppiamento. Un potenziale motivo per utilizzare la programmazione orientata agli oggetti è il riutilizzo. Quindi, ad esempio, puoi dividere l'algoritmo tra due oggetti collaborativi A e B. Questo potrebbe essere utile per la successiva creazione di un altro algoritmo, che potrebbe riutilizzare l'uno o l'altro dei due oggetti. Tuttavia, quando tali oggetti comunicano (invia messaggi - chiama metodi), creano dipendenze tra loro. Ma se si desidera utilizzare l'uno senza l'altro, è necessario specificare cosa dovrebbe fare un altro oggetto C per l'oggetto A se si sostituisce B. Tali descrizioni sono chiamate interfacce. Ciò consente all'oggetto A di comunicare senza modifiche con un oggetto diverso basandosi sull'interfaccia. L'affermazione che hai citato dice che se prevedi di riutilizzare una parte di un algoritmo (o più in generale un programma), devi creare interfacce e fare affidamento su di esse,


2

Come altri hanno già detto, significa che il tuo codice chiamante dovrebbe conoscere solo un genitore astratto, NON l'attuale classe di implementazione che farà il lavoro.

Ciò che aiuta a capire questo è il PERCHÉ dovresti 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 classe. Se il mio programma conosce la classe concreta, posso solo testare il mio codice eseguendolo realmente contro quella classe. Sto usando -> per indicare "parla con".

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

WorkerClass -> IDAL -> DALClass.

Quindi DALClass implementa l'interfaccia IDAL e la classe operaia chiama SOLO tramite questo.

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

WorkerClass -> IDAL -> IFakeDAL.

2) Riutilizzo

Seguendo l'esempio sopra, diciamo che vogliamo passare da SQL Server (che utilizza la nostra DALClass concreta) a MonogoDB. Ciò richiederebbe un lavoro importante, ma NON se avessimo programmato un'interfaccia. In tal caso, scriviamo solo la nuova classe DB e cambiamo (via factory)

WorkerClass -> IDAL -> DALClass

per

WorkerClass -> IDAL -> MongoDBClass


1

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

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.