webentwicklung-frage-antwort-db.com.de

Was bedeutet "Programmieren an Schnittstellen, keine Implementierungen"?

Man stößt auf diesen Satz, wenn man über Designmuster liest.

Aber ich verstehe es nicht, könnte mir das jemand erklären?

115

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

Codierung gegen Interface bedeutet, dass der Client-Code immer ein Interface-Objekt enthält, das von einer Fabrik geliefert wird. Jede Instanz, die von der Factory zurückgegeben wird, ist vom Typ Interface, den jede Factory-Kandidatenklasse implementiert haben muss. Auf diese Weise kümmert sich das Client-Programm nicht um die Implementierung, und die Schnittstellensignatur bestimmt, was für 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, unter dem Gesichtspunkt 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 einfaches 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 Basisklasse für abstrakte Sprecher hinzugefügt. In diesem Update habe ich "SayHello" zu allen Spakers hinzugefügt. Alle Sprecher sprechen "Hallo Welt". Das ist also ein gemeinsames Merkmal mit ähnlicher Funktion. Im Klassendiagramm finden Sie die ISpeaker-Schnittstelle der abstrakten Klasse "Speaker" und das Symbol "Speak" () "als abstrakt". Dies bedeutet, dass die Implementierung der Speak-Methode von Sprecher zu Sprecher unterschiedlich ist. Aber alle Redner sagen einstimmig "Hallo". In der Klasse "Abstract Speaker" definieren wir eine Methode mit dem Namen "Hello World". Jede Implementierung von Speaker leitet die SayHello-Methode ab.

Stellen Sie sich einen Fall vor, in dem SpanishSpeaker nicht Hallo sagen kann. In diesem Fall können Sie die SayHello-Methode für Spanish Speaker außer Kraft setzen und die richtige Ausnahme auslösen.

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

Und wir könnten dieses Verhalten erreichen, indem wir einfach eine abstrakte Basisklasse Speaker und einige geringfügige Änderungen in Each-Implementierung hinzufügen und so das ursprüngliche Programm unverändert lassen. Dies ist ein gewünschtes Merkmal 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

131

Stellen Sie sich eine Schnittstelle als einen Vertrag zwischen einem Objekt und seinen Kunden vor. Das heißt, die Schnittstelle gibt die Funktionen eines Objekts und die Signaturen für den Zugriff auf diese Funktionen an.

Implementierungen sind das eigentliche Verhalten. Angenommen, Sie haben eine Methode sort (). Sie können QuickSort oder MergeSort implementieren. Dies sollte für die aufrufende Sortierung des Client-Codes keine Rolle spielen, solange sich die Schnittstelle nicht ändert.

Bibliotheken wie die Java API und das .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 nicht ändern für die Klassen in diesen Bibliotheken, da dies alle Programmierer betrifft, die die Bibliothek verwenden. Andererseits können sie die Implementierung beliebig ändern.

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

  1. es verbirgt die Dinge, die Sie nicht wissen müssen, um die Verwendung des Objekts zu vereinfachen.
  2. es liefert den Vertrag darüber, wie sich das Objekt verhält, sodass Sie sich darauf verlassen können
26

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

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 Interface-/Abstract-Klasse) verwendet wird, werden die Aufrufe für das Objekt ausgeführt.

Dies ist eine Teilmenge der Liskov Substitution Principle (LSP), das L der SOLID -Prinzipien.

Ein Beispiel in .NET wäre, mit IList anstelle von List oder Dictionary zu codieren, sodass Sie jede Klasse verwenden könnten, die IList austauschbar in Ihrem Code implementiert :

// 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 abstrakte Klasse ProviderBase . Dies bietet eine gewisse Infrastruktur und bedeutet, dass alle Anbieterimplementierungen austauschbar verwendet werden können, wenn Sie Code dagegen verwenden.

15
Oded

Wenn Sie eine Autoklasse 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 Car-Klasse zu haben und Details in der entsprechenden Implementierung zu verbergen. Jeder Autotyp würde eine eigene Implementierung für performMaintenance () bereitstellen. Als Besitzer eines Autos müssen Sie sich nur mit performMaintenance () befassen 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 stellen den Service zusammen, 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 stellen einen Mechanismus zur Bereitstellung des Dienstes bereit.
  3. Sie möchten sich keine Gedanken über die Zuordnung des Fahrzeugtyps zum Dienstanbieter machen. Sie geben nur an, wann Sie die Wartung planen möchten, und rufen sie auf. Das zuständige 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, um den Dienst bereitzustellen. Grundsätzlich übernehmen Sie die Implementierung.
  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 zur Durchführung der Wartung zu finden. Ihre Aufgabe ist es, das Auto zu fahren und Spaß daran zu haben. Nicht im Geschäft zu sein, es zu pflegen.

    Was ist der Nachteil des ersten Ansatzes? Es ist mit dem Aufwand verbunden, ein Unternehmen zu finden usw. Wenn Sie kein Mietwagenunternehmen sind, lohnt sich der Aufwand möglicherweise nicht.

5
Raghav Navada

Bei dieser Aussage geht es um 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 - Methoden aufrufen), erzeugen sie Abhängigkeiten untereinander. Wenn Sie jedoch eins 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. Auf diese Weise kann Objekt A ohne Änderung mit einem anderen Objekt kommunizieren, das sich auf die Schnittstelle stützt. Die von Ihnen erwähnte Aussage besagt, dass Sie, wenn Sie einen Teil eines Algorithmus (oder allgemeiner ein Programm) wiederverwenden möchten, Schnittstellen erstellen und sich auf diese verlassen sollten, damit Sie die konkrete Implementierung jederzeit ändern können, ohne andere Objekte zu ändern, wenn Sie das verwenden deklarierte Schnittstelle.

4

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 erledigen wird.

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

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 gegen diese Klasse ausführe. Ich benutze -> um "Gespräche mit" zu bedeuten.

WorkerClass -> DALClass Fügen wir der Mischung 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 wie eine Datenbank verhält.

WorkerClass -> IDAL -> IFakeDAL.

2) Wiederverwenden

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

WorkerClass -> IDAL -> DALClass

zu

WorkerClass -> IDAL -> MongoDBClass

2
Mathieson

schnittstellen beschreiben Fähigkeiten. Wenn Sie imperativen Code schreiben, sprechen Sie über die Funktionen, die Sie verwenden, und nicht über bestimmte Typen oder Klassen.

1
rektide