Man stößt auf diesen Satz, wenn man über Designmuster liest.
Aber ich verstehe es nicht, könnte mir das jemand erklären?
Man stößt auf diesen Satz, wenn man über Designmuster liest.
Aber ich verstehe es nicht, könnte mir das jemand erklären?
Antworten:
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
}
Dies ist nur ein grundlegendes Beispiel, und die tatsächliche Erklärung des Prinzips würde den Rahmen dieser Antwort sprengen.
Ich habe das obige Beispiel aktualisiert und eine abstrakte Speaker
Basisklasse 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 Speaker
abstrakte Klasse die ISpeaker
Schnittstelle implementiert und Speak()
als abstrakt markiert. Dies bedeutet, dass jede Speaker-Implementierung für die Implementierung der Speak()
Methode verantwortlich ist, da sie von Speaker
bis variiert Speaker
. Aber alle Redner sagen einstimmig "Hallo". In der abstrakten Speaker-Klasse definieren wir eine Methode mit der Aufschrift "Hello World", und jede Speaker
Implementierung leitet die SayHello()
Methode ab.
Stellen Sie sich einen Fall vor, in dem Sie SpanishSpeaker
nicht 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
}
List
als Typ verwenden, können Sie immer noch davon ausgehen, dass der Direktzugriff durch wiederholtes Aufrufen schnell ist get(i)
.
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:
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 SOLID
Prinzipien.
Ein Beispiel in .NET wäre das Codieren mit IList
anstelle von List
oder Dictionary
, sodass Sie jede Klasse verwenden können, die IList
austauschbar 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 ProviderBase
abstrakte Klasse - dies bietet eine gewisse Infrastruktur und bedeutet ebenso wichtig, dass alle Anbieterimplementierungen austauschbar verwendet werden können, wenn Sie dagegen codieren.
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.
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.
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.
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.
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