Was bedeutet "Programmieren auf Schnittstellen, nicht Implementierungen"?

Antworten:

148

Schnittstellen sind nur Verträge oder Signaturen und sie wissen nichts über Implementierungen.

Codierung gegen Schnittstelle bedeutet, dass der Client-Code immer ein Schnittstellenobjekt enthält, das von einer Fabrik geliefert wird. Jede von der Factory zurückgegebene Instanz wäre vom Typ Interface, den jede Factory-Kandidatenklasse implementiert haben muss. Auf diese Weise macht sich das Client-Programm keine Sorgen um die Implementierung und die Schnittstellensignatur bestimmt, was alle Vorgänge ausgeführt werden können. Dies kann verwendet werden, um das Verhalten eines Programms zur Laufzeit zu ändern. Es hilft Ihnen auch, aus Sicht der Wartung weitaus bessere Programme zu schreiben.

Hier ist ein einfaches Beispiel für Sie.

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

Dies ist nur ein grundlegendes Beispiel, und die tatsächliche Erklärung des Prinzips würde den Rahmen dieser Antwort sprengen.

BEARBEITEN

Ich habe das obige Beispiel aktualisiert und eine abstrakte SpeakerBasisklasse hinzugefügt . In diesem Update habe ich "SayHello" allen Lautsprechern eine Funktion hinzugefügt. Alle Sprecher sprechen "Hallo Welt". Das ist also ein gemeinsames Merkmal mit ähnlicher Funktion. Wenn Sie sich das Klassendiagramm ansehen, werden Sie feststellen, dass die Speakerabstrakte Klasse die ISpeakerSchnittstelle implementiert und Speak()als abstrakt markiert. Dies bedeutet, dass jede Speaker-Implementierung für die Implementierung der Speak()Methode verantwortlich ist, da sie von Speakerbis variiert Speaker. Aber alle Redner sagen einstimmig "Hallo". In der abstrakten Speaker-Klasse definieren wir eine Methode mit der Aufschrift "Hello World", und jede SpeakerImplementierung leitet die SayHello()Methode ab.

Stellen Sie sich einen Fall vor, in dem Sie SpanishSpeakernicht Hallo sagen können. In diesem Fall können Sie die SayHello()Methode für Spanisch sprechend überschreiben und die richtige Ausnahme auslösen.

Bitte beachten Sie, dass wir keine Änderungen an Interface ISpeaker vorgenommen haben. Auch der Client-Code und SpeakerFactory bleiben unverändert. Und das erreichen wir durch Programming-to-Interface .

Und wir könnten dieses Verhalten erreichen, indem wir einfach einen Sprecher der abstrakten Basisklasse und einige geringfügige Änderungen in jeder Implementierung hinzufügen, wodurch das ursprüngliche Programm unverändert bleibt. Dies ist eine gewünschte Funktion jeder Anwendung und macht Ihre Anwendung leicht wartbar.

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

Dies. __curious_geek
quelle
19
Bei der Programmierung auf die Schnittstelle geht es nicht nur um den Typ der Referenzvariablen. Dies bedeutet auch, dass Sie keine impliziten Annahmen über Ihre Implementierung verwenden. Wenn Sie beispielsweise a Listals Typ verwenden, können Sie immer noch davon ausgehen, dass der Direktzugriff durch wiederholtes Aufrufen schnell ist get(i).
Joachim Sauer
16
Fabriken sind orthogonal zur Programmierung auf Schnittstellen, aber ich denke, diese Erklärung lässt es scheinen, als ob sie Teil davon sind.
T.
@Toon: stimme dir zu. Ich wollte ein sehr einfaches und einfaches Beispiel für die Programmierung zur Schnittstelle liefern. Ich wollte den Fragesteller nicht verwirren, indem ich eine IFlyable-Schnittstelle für einige Vogel- und Tierklassen implementierte.
das. __curious_geek
@Dies. Wenn ich stattdessen eine abstrakte Klasse oder ein Fassadenmuster verwende, heißt es dann immer noch "Programm für eine Schnittstelle"? oder muss ich explizit eine schnittstelle verwenden und in einer klasse implementieren?
nie_had_a_name
1
Mit welchem ​​Uml-Tool haben Sie die Bilder erstellt?
Adam Arold
29

Stellen Sie sich eine Schnittstelle als Vertrag zwischen einem Objekt und seinen Kunden vor. Das heißt, die Schnittstelle gibt die Dinge an, die ein Objekt tun kann, und die Signaturen für den Zugriff auf diese Dinge.

Implementierungen sind die tatsächlichen Verhaltensweisen. Angenommen, Sie haben eine Methode sort (). Sie können QuickSort oder MergeSort implementieren. Dies sollte für den Clientcode, der sort aufruft, keine Rolle spielen, solange sich die Schnittstelle nicht ändert.

Bibliotheken wie die Java-API und .NET Framework verwenden häufig Schnittstellen, da Millionen von Programmierern die bereitgestellten Objekte verwenden. Die Ersteller dieser Bibliotheken müssen sehr vorsichtig sein, dass sie die Schnittstelle zu den Klassen in diesen Bibliotheken nicht ändern, da dies alle Programmierer betrifft, die die Bibliothek verwenden. Auf der anderen Seite können sie die Implementierung beliebig ändern.

Wenn Sie als Programmierer gegen die Implementierung codieren, funktioniert Ihr Code nicht mehr, sobald er sich ändert. Stellen Sie sich die Vorteile der Benutzeroberfläche folgendermaßen vor:

  1. Es verbirgt die Dinge, die Sie nicht wissen müssen, um die Verwendung des Objekts zu vereinfachen.
  2. Es enthält den Vertrag über das Verhalten des Objekts, sodass Sie sich darauf verlassen können
Vincent Ramdhanie
quelle
Dies bedeutet, dass Sie wissen müssen, wofür Sie das Objekt unter Vertrag nehmen: In dem Beispiel, in dem Sie nur eine Sortierung abschließen, nicht unbedingt eine stabile Sortierung.
Pinguat
Ähnlich wie in der Bibliotheksdokumentation die Implementierung nicht erwähnt wird, handelt es sich lediglich um Beschreibungen der enthaltenen Klassenschnittstellen.
Joe Iddon
17

Dies bedeutet, dass Sie versuchen sollten, Ihren Code so zu schreiben, dass er eine Abstraktion (abstrakte Klasse oder Schnittstelle) anstelle der direkten Implementierung verwendet.

Normalerweise wird die Implementierung über den Konstruktor oder einen Methodenaufruf in Ihren Code eingefügt. Ihr Code kennt also die Schnittstelle oder die abstrakte Klasse und kann alles aufrufen, was in diesem Vertrag definiert ist. Wenn ein tatsächliches Objekt (Implementierung der Schnittstelle / abstrakte Klasse) verwendet wird, werden die Aufrufe für das Objekt ausgeführt.

Dies ist eine Teilmenge des Liskov Substitution Principle(LSP), des L der SOLIDPrinzipien.

Ein Beispiel in .NET wäre das Codieren mit IListanstelle von Listoder Dictionary, sodass Sie jede Klasse verwenden können, die IListaustauschbar in Ihrem Code implementiert ist :

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

Ein weiteres Beispiel aus der Basisklassenbibliothek (BCL) ist die ProviderBaseabstrakte Klasse - dies bietet eine gewisse Infrastruktur und bedeutet ebenso wichtig, dass alle Anbieterimplementierungen austauschbar verwendet werden können, wenn Sie dagegen codieren.

Oded
quelle
Aber wie kann ein Client mit einer Schnittstelle interagieren und ihre leeren Methoden verwenden?
nie_had_a_name
1
Der Client interagiert nicht mit der Schnittstelle, sondern über die Schnittstelle :) Objekte interagieren mit anderen Objekten über Methoden (Nachrichten) und eine Schnittstelle ist eine Art Sprache - wenn Sie wissen, dass bestimmte Objekte (Personen) Englisch (IList) implementieren (sprechen) ) können Sie es verwenden, ohne mehr über dieses Objekt wissen zu müssen (dass er auch Italiener ist), da es in diesem Zusammenhang nicht benötigt wird (wenn Sie um Hilfe bitten möchten, müssen Sie nicht wissen, dass er auch Italienisch spricht wenn Sie Englisch verstehen).
Gabriel Ščerbák
Übrigens. Das IMHO-Liskov-Substitutionsprinzip handelt von der Vererbungssemantik und hat nichts mit Schnittstellen zu tun, die auch in Sprachen ohne Vererbung zu finden sind (Go from Google).
Gabriel Ščerbák
5

Wenn Sie eine Auto-Klasse in der Combustion-Car-Ära schreiben, besteht eine große Chance, dass Sie oilChange () als Teil dieser Klasse implementieren. Wenn Elektroautos eingeführt werden, sind Sie jedoch in Schwierigkeiten, da für diese Autos kein Ölwechsel und keine Implementierung erforderlich ist.

Die Lösung des Problems besteht darin, eine performMaintenance () - Schnittstelle in der Fahrzeugklasse zu haben und Details in der entsprechenden Implementierung auszublenden. Jeder Fahrzeugtyp würde eine eigene Implementierung für performMaintenance () bereitstellen. Als Besitzer eines Autos müssen Sie sich nur um performMaintenance () kümmern und müssen sich nicht um Anpassungen kümmern, wenn es zu einer ÄNDERUNG kommt.

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

Zusätzliche Erklärung: Sie sind ein Autobesitzer, der mehrere Autos besitzt. Sie arbeiten den Service aus, den Sie auslagern möchten. In unserem Fall wollen wir die Wartungsarbeiten aller Autos auslagern.

  1. Sie identifizieren den Vertrag (Schnittstelle), der für alle Ihre Autos und Dienstleister gilt.
  2. Dienstanbieter bieten einen Mechanismus zur Bereitstellung des Dienstes an.
  3. Sie möchten sich nicht darum kümmern, den Fahrzeugtyp dem Dienstanbieter zuzuordnen. Sie geben nur an, wann Sie die Wartung planen und aufrufen möchten. Ein geeignetes Serviceunternehmen sollte einspringen und die Wartungsarbeiten durchführen.

    Alternativer Ansatz.

  4. Sie identifizieren die Arbeit (kann eine neue Schnittstelle sein), die für alle Ihre Autos gilt.
  5. Sie kommen mit einem Mechanismus heraus, um den Dienst bereitzustellen. Grundsätzlich werden Sie die Implementierung bereitstellen.
  6. Sie rufen die Arbeit auf und erledigen sie selbst. Hier erledigen Sie die entsprechenden Wartungsarbeiten.

    Was ist der Nachteil des 2. Ansatzes? Sie sind möglicherweise nicht der Experte, wenn es darum geht, den besten Weg für die Wartung zu finden. Ihre Aufgabe ist es, das Auto zu fahren und es zu genießen. Nicht im Geschäft zu sein, es aufrechtzuerhalten.

    Was ist der Nachteil des ersten Ansatzes? Es gibt den Aufwand, eine Firma usw. zu finden. Wenn Sie keine Mietwagenfirma sind, lohnt sich die Mühe möglicherweise nicht.

Raghav Navada
quelle
4

In dieser Aussage geht es um die Kopplung. Ein möglicher Grund für die Verwendung der objektorientierten Programmierung ist die Wiederverwendung. So können Sie beispielsweise Ihren Algorithmus auf zwei zusammenarbeitende Objekte A und B aufteilen. Dies kann nützlich sein, um später einen anderen Algorithmus zu erstellen, der das eine oder andere der beiden Objekte wiederverwenden kann. Wenn diese Objekte jedoch kommunizieren (Nachrichten senden - Aufrufmethoden), erzeugen sie Abhängigkeiten untereinander. Wenn Sie jedoch eines ohne das andere verwenden möchten, müssen Sie angeben, was ein anderes Objekt C für Objekt A tun soll, wenn wir B ersetzen. Diese Beschreibungen werden als Schnittstellen bezeichnet. Dies ermöglicht es Objekt A, unverändert mit verschiedenen Objekten zu kommunizieren, die auf der Schnittstelle beruhen. Die von Ihnen erwähnte Aussage besagt, dass Sie Schnittstellen erstellen und sich auf diese verlassen sollten, wenn Sie einen Teil eines Algorithmus (oder allgemeiner ein Programm) wiederverwenden möchten.

Gabriel Ščerbák
quelle
2

Wie andere gesagt haben, bedeutet dies, dass Ihr aufrufender Code nur über ein abstraktes übergeordnetes Element Bescheid wissen sollte, NICHT über die eigentliche implementierende Klasse, die die Arbeit erledigt.

Was hilft, dies zu verstehen, ist das WARUM Sie immer auf eine Schnittstelle programmieren sollten. Es gibt viele Gründe, aber zwei der am einfachsten zu erklärenden sind

1) Testen.

Angenommen, ich habe meinen gesamten Datenbankcode in einer Klasse. Wenn mein Programm die konkrete Klasse kennt, kann ich meinen Code nur testen, indem ich ihn wirklich für diese Klasse ausführe. Ich benutze ->, um "Gespräche mit" zu bedeuten.

WorkerClass -> DALClass Fügen wir dem Mix jedoch eine Schnittstelle hinzu.

WorkerClass -> IDAL -> DALClass.

Die DALClass implementiert also die IDAL-Schnittstelle, und die Worker-Klasse ruft NUR diese auf.

Wenn wir nun Tests für den Code schreiben möchten, können wir stattdessen eine einfache Klasse erstellen, die sich nur wie eine Datenbank verhält.

WorkerClass -> IDAL -> IFakeDAL.

2) Wiederverwendung

Nehmen wir an, wir möchten nach dem obigen Beispiel von SQL Server (den unsere konkrete DALClass verwendet) zu MonogoDB wechseln. Dies würde große Arbeit erfordern, aber NICHT, wenn wir auf eine Schnittstelle programmiert haben. In diesem Fall schreiben wir einfach die neue DB-Klasse und ändern (über die Factory)

WorkerClass -> IDAL -> DALClass

zu

WorkerClass -> IDAL -> MongoDBClass

Mathieson
quelle
1

Schnittstellen beschreiben Fähigkeiten. Sprechen Sie beim Schreiben von imperativem Code über die von Ihnen verwendeten Funktionen und nicht über bestimmte Typen oder Klassen.

rektide
quelle