it-swarm.com.de

Sollte ich trotzdem "Programmieren auf eine Schnittstelle ohne Implementierung" befolgen, auch wenn ich denke, dass die Verwendung konkreter Klassenmitglieder die einfachere Lösung ist?

Nach Verständnis von "Programmieren auf eine Schnittstelle" , so wie ich es verstehe, sollte ich mich nur auf die abstrakte Klasse verlassen. In einigen Fällen jedoch zum Beispiel Student:

public class Student {
    private String name;
    private int age;
}

um es so zu ändern, dass es nur von der abstrakten Klasse abhängt (welche MyIString eine neue abstrakte Klasse sein kann, die String umschließt):

public class Student {
    private MyIString name;
    private Java.lang.Number age;
}

Ich denke, das modifizierte ist komplexer. Und ein "realeres" Beispiel, sagen Sie Address:

public class Address {
    private ZipCode zipcode;
}

ich brauche nur einen Typ von ZipCode, aber wenn ich ihn ändere als:

public class Address {
    private IZipCode zipcode;
}

welches IZipCode eine Schnittstelle ist, dann denke ich, dass es andere Teamkollegen irreführen kann, dass es andere Arten von ZipCodes geben würde.

Ich denke, die oben genannten Fälle werden komplexer und weniger wartbar, wenn ich einer Klasse nur abstrakte Klassenmitglieder verwenden durfte. Meine Frage ist also, sollte ich immer noch "Programmieren auf eine Schnittstelle, nicht Implementierung" folgen, wenn die "Verfolgte" komplexer wird (meiner Ansicht nach)?

34
aacceeggiikk

Programmieren auf eine Schnittstelle bedeutet, dass Sie sich auf konzentrieren sollten, was der Code tut, nicht wie es tatsächlich implementiert ist. Siehe Telastyns Antwort auf das Verständnis von „Programmieren auf eine Schnittstelle“ . Schnittstellenklassen helfen bei der Durchsetzung dieser Richtlinie. Dies bedeutet jedoch nicht, dass Sie niemals konkrete Klassen verwenden sollten.

Das Beispiel für die Postleitzahl gefällt mir sehr gut:

1963 führt der United States Postal Service Postleitzahlen ein, die aus fünf Ziffern bestehen. Möglicherweise haben Sie jetzt die (schlechte) Vorstellung, dass eine Ganzzahl eine ausreichende Darstellung ist, und verwenden sie im gesamten Code. 1983 wird ein neues Postleitzahlformat eingeführt. Es werden 5 Ziffern, ein Bindestrich und 4 weitere Ziffern verwendet. Plötzlich sind Postleitzahlen keine Ganzzahlen mehr und Sie müssen alle Stellen, an denen Ganzzahlen für Postleitzahlen verwendet werden, in etwas anderes ändern, z. B. in Zeichenfolgen. Dieses Refactoring hätte vermieden werden können, wenn eine bestimmte Klasse ZipCode verwendet worden wäre. Dann musste nur die interne Darstellung der Klasse ZipCode geändert werden, und ihre Verwendung wäre gleich geblieben.

Die Verwendung einer ZipCode -Klasse ist bereits Programmierung auf eine Schnittstelle: Anstatt gegen eine konkrete Implementierung ("integer"/"string") zu programmieren, programmieren Sie gegen eine Abstraktion ("Zip") Code").

Bei Bedarf können Sie eine weitere Abstraktionsebene hinzufügen: Möglicherweise müssen Sie verschiedene Postleitzahlenformate für verschiedene Länder unterstützen. Sie können beispielsweise eine Schnittstelle IPostalCode mit Implementierungen wie PostalCodeUS (dies ist nur die ZipCode von oben), PostalCodeIreland, PostalCodeNetherlands hinzufügen ] usw. Zu viele Abstraktionsebenen können den Code jedoch auch komplexer und schwieriger zu überlegen machen. Ich denke, es hängt von den Anforderungen Ihrer Anwendung ab, ob Sie eine ZipCode - oder eine IPostalCode Schnittstelle. Möglicherweise ist eine ZipCode, die intern eine (Unicode-) Zeichenfolge verwendet, für alle Länder gut genug, und länderspezifische Dinge, z. B. die Validierung, können in einigen PostalCodeValidator/IPostalCodeValidator.

76
pschill

"Programmieren auf eine Schnittstelle" erfordert nicht das Sprachschlüsselwort interface. Es bedeutet, dass Sie sich darum kümmern, was der Typ über sein Verhalten verspricht.

Es ist dir egal wieJava.lang.String macht alles, was es macht, nur dass du schreiben kannst

name = "aacceeggiikk";
age = 42;

"Programmieren auf eine Implementierung" würde Reflexion verwenden, um die privaten Mitglieder von String zu erreichen und mit ihnen herumzuspielen. Und dann war ich verärgert darüber, dass ein Update Ihren Code beschädigt hat, weil Sie sich auf etwas verlassen haben, das geändert wurde.

47
Caleth

Es ist nur eine Richtlinie

Genau wie ein Konzept wie KUSS eine Richtlinie ist.

Wenn Sie der Richtlinie "Programmieren auf eine Schnittstelle, nicht auf eine Implementierung" folgen, verstoßen Sie gegen das Prinzip KISS). Indem Sie jedoch eine Reihe einfacher Klassen erstellen, die Code oder Aktionen wiederholen, Sie würden das TROCKENE Prinzip verletzen.

Es gibt keine Möglichkeit, sich an jede einzelne Richtlinie, jedes Prinzip oder jede "Regel" zu halten. Sie müssen herausfinden, welches Gleichgewicht für die Anwendung your Sinn macht.

Meine eigene Erfahrung ist, dass der Versuch, alles mit abstrakten Ebenen auf abstrahierten Ebenen unternehmenssicher zu machen, oft übertrieben und für viele Anwendungen unnötig komplex ist.

10
Ivo Coumans
  1. Die Programmierung auf eine Schnittstelle betrifft Objekte, Entitäten, die auch primitiv typisierte Eigenschaften wie boolean, int, BigDecimal und String haben können, um nur einige zu nennen. Auf dieser Ebene gilt die Programmierung auf eine Schnittstelle nicht mehr.

  2. Es gibt jedoch ein anderes Prinzip, das auf der Erfahrung basiert, dass primitive Typen verwendet werden, bei denen ein Wrapper für eine abstraktere Werteklasse vorhanden sein sollte. Also stattdessen

    String telephone;
    

    vielleicht haben Sie besser einen Wert Klasse Telefon:

    Telephone telephone.
    

    Dies kann sich um die Ländernummer, die kanonische Form, die Gleichheit und dergleichen kümmern. Wieder könnte Telefon eine Schnittstelle sein und es könnte eine implementierende Klasse TelephoneImpl geben, aber ich sehe dies - zum Glück - selten.

    Noch besser ist es, wenn mehr als ein primitives Feld durch eine abstraktere Klasse ersetzt werden kann, wie z. B. FullName, beispielsweise mit int similarityPercentage(FullName other).

  3. Vielleicht sollte erwähnt werden, dass interne Objekte mit internen Implementierungen Schnittstellen verwenden können. Ein List hat eine Schnittstelle Iterator iterator(), die ein lokales Objekt mit Zugriff auf den Listencontainer implementiert. Aber hier ist die Schnittstelle Teil der Lösung, nicht der Codierungsstil.

Kurz gesagt:

Die Programmierung über die Schnittstelle entkoppelt die Implementierungsklasse, ermöglicht mehrere Implementierungen und hat eine einzige Verantwortung als API. Es geht um Verhalten, Methoden. Nehmen Sie als Beispiel JDBC.

Felder sind konkrete Eigenschaften. Sie sind Daten . Sie sollten auch abstrahiert werden und die Implementierung unter Verwendung eines Klassennamens verbergen und Zugriffsmethoden für eine korrekte Verwendung bereitstellen. Das ist jedoch eine andere Ebene. Nehmen Sie die Klassen LocalDate, LocalDateTime, YearMonth anstelle des alten Datums für alle.

7
Joop Eggen

Um einen meiner Kollegen zu zitieren: " Sie können Stunden damit verbringen, ein Auto am Computer zu entwerfen, aber irgendwann müssen die Räder tatsächlich den Boden berühren.".

d.h. nicht alles kann/sollte abstrahiert werden, irgendwann brauchen Sie einen konkreten Typ.

Für Ihre Klasse mit Ihrer Postleitzahl sollte die Frage wirklich sein, was ist eine Postleitzahl (in Bezug auf Ihre Bewerbung)?

Antwort - es ist eine Zeichenfolge, daher sollte die Eigenschaft eine Zeichenfolge sein.

public class Address {
     public String ZipCode...
}

P.S. Fangen Sie nicht an, die zugrunde liegenden Sprachtypen zu abstrahieren - auf diese Weise liegt der Wahnsinn!

Es gibt hier eine Reihe von Antworten/Kommentaren, die die Vielfalt der Postleitzahlen in verschiedenen Ländern kommentieren, und das ist wahr, aber letztendlich sind sie alle Zeichenfolgen.

Hoffentlich ist es dieser Adressklasse egal, was in der Postleitzahl steht (nur dass es sich um eine Zeichenfolge handelt), und es gibt eine andere Klasse, mit der Sie Ihre Adresse validieren (und die Implementierung von das Klasse kann durchaus nützlich sein).

Programmieren Sie also auf eine Schnittstelle, auf der dies sinnvoll ist und insbesondere dort, wo die Implementierung möglicherweise geändert oder das Verhalten injiziert werden muss. Verwenden Sie nicht für alles eine Schnittstelle!

6
Paddy

In Ihrem Adress-/Postleitzahl-Beispiel:

public class Address {
private ZipCode zipcode;
}

Wenn Sie sich an eine konkrete Implementierung anstelle einer Schnittstelle binden, treten drei Probleme auf:

  • A - Internationalisierung - Ein z.B. Die kanadische Postleitzahl ist so etwas wie V1C3X8, die Adressen anderer Länder sind wieder anders. Viel Glück beim Speichern der Rechnungsadressen Ihrer Kunden in einer globalisierten Wirtschaft.
  • B - Eine Änderung in der Implementierung, weshalb alles als Schnittstelle verwendet werden sollte - Sie könnten eines Tages entscheiden, dass es effizienter ist, Adressen als GeoCode mit drei Wörtern zu speichern, der als Zahl und Etage gespeichert ist/Einheitennummer für mehrstöckiges Gebäude. Daher könnte die Adresse wie folgt geändert werden:
public class Address : IAddress 
{
    public IZipCode ZipCode{get; private set;}
    public string StreetName{get;private set}
}

Zu so etwas wie:

public class Address : IAddress 
{
    public Address(IGeoCodeService geoCodeService/*Dependency injected*/){........}

    private long What3WordsGeoCodeAsLong {get; set;}

    public IZipCode ZipCode => GetZipCodeFromGeoCode();//calls out to a service to 
    public string StreetName => GetStreetNameFromGeoCode();
}

Bei der Serialisierung ist die zweite Implementierung viel kleiner zu speichern und nicht veraltet, wenn ein Land beschließt, Postleitzahlenbereiche zu verschieben, was häufiger vorkommt als Sie denken (mit einem Kompromiss) von einem Dienst, um jedes Mal eine Postleitzahl nachzuschlagen.), während alles, das IAddress verwendet, nicht einmal "bewusst" ist, dass die Implementierung jetzt völlig anders ist!

  • C - Unit-Tests, ich sehe, dass Sie eine .Net-Namenskonvention verwenden. Mir ist kein .NET-Testframework bekannt, mit dem Sie die Methoden für Adresse und Postleitzahl in Klassen verspotten können, in denen sie verwendet werden. Sie müssen sie als IA-Adresse und IZipcode verwenden.
0
Eugene

Dies ist meine Einstellung zu "Programmieren auf eine Schnittstelle". Es gibt zwei Teile: Ein- und Ausgänge.

Eingänge

Ich unterscheide zwischen Objekten, die Daten sind, und Objekten, die Verhalten haben, insbesondere Nebenwirkungen.

In beiden Beispielen würde ich die konkreten Klassen verwenden: Zeichenfolgen und Postleitzahlen sind Daten, IMO. (Ich würde den Postleitzahl NICHT einfach zu einem String machen, wie in einer der Antworten vorgeschlagen !!! Sie können keinen alten String übergeben und wie einen Postleitzahl behandeln. Ihre Postleitzahlklasse sollte unterschiedliche Konstruktoren für die verschiedenen Arten von Postleitzahlen haben, die Sie möglicherweise verwenden beschäftigen sich mit. Intern können Sie eine Zeichenfolge speichern, aber zumindest eine Validierung in den Konstruktoren durchführen!).

Wenn ich jedoch ein Objekt benötigen würde, das irgendeine Art von "Computing" ausführt (ein vager Begriff, ich weiß), würde ich eine Schnittstelle verwenden. Wenn ich beispielsweise ein Objekt benötige, mit dem ich nach Schülern nach Namen suchen kann, würde ich eine Schnittstelle zur Eingabe verwenden. Die Schnittstelle wäre so etwas wie:

interface StudentSearchInterface {
    Optional<Student> search(String query);
}

Der Grund, warum dies eine gute Idee ist, liegt darin, dass Sie die Anbieter später austauschen können und außerdem viel bessere Tests schreiben können, in denen Sie "gefälschte" StudentSearchInterface-Objekte codieren können, die genau das zurückgeben, was Sie für Ihren spezifischen Test wünschen. Es kann schwierig sein, die Produktionsklasse zu testen, die StudentSearchInterface implementiert: Was ist, wenn ein Server im Internet abgefragt wird? Oder liest aus einer Datenbank? Die Ergebnisse können sich zwischen den Aufrufen von search() ändern, sodass Sie sie nicht wirklich testen können.

Ausgänge

Ihre Rückgabewerte sollten entweder "Daten" oder eine Schnittstelle sein. Mit Daten meine ich eine Klasse, die wirklich nur Getter und/oder Setter und möglicherweise einige triviale Methoden wie toString() usw. hat. Andernfalls machen Sie eine Schnittstelle. Ein häufiges Beispiel ist die Rückgabe eines List<Foo> Anstelle eines ArrayList<Foo>. Dies gibt Ihnen die Freiheit, die Logik in Ihrer Methode zu ändern, um etwas anderes zu verwenden, das List implementiert, anstatt an einen bestimmten List-Implementierer gebunden zu sein, da Sie dies zuerst verwendet haben.

Genau wie wir einige Methoden und Felder als private markieren, um sie zu kapseln, sollten wir Schnittstellen verwenden, um Informationen nur auf der Basis von "Need to Know" bereitzustellen.

0
R. Agnese