Ist es immer sinnvoll, in Java auf eine Schnittstelle zu programmieren?

9

Ich habe die Diskussion bei dieser Frage gesehen, wie eine Klasse, die von einer Schnittstelle implementiert wird, instanziiert wird. In meinem Fall schreibe ich ein sehr kleines Programm in Java, das eine Instanz von verwendet TreeMap, und nach Meinung aller sollte es wie folgt instanziiert werden:

Map<X> map = new TreeMap<X>();

In meinem Programm rufe ich die Funktion auf map.pollFirstEntry(), die nicht in der MapSchnittstelle deklariert ist (und einige andere, die auch in der MapSchnittstelle vorhanden sind). Ich habe es geschafft, indem TreeMap<X>ich auf eine Stelle gewirkt habe, die ich diese Methode nenne:

someEntry = ((TreeMap<X>) map).pollFirstEntry();

Ich verstehe die Vorteile der oben beschriebenen Initialisierungsrichtlinien für große Programme, aber für ein sehr kleines Programm, bei dem dieses Objekt nicht an andere Methoden übergeben wird, halte ich es für unnötig. Trotzdem schreibe ich diesen Beispielcode als Teil einer Bewerbung und möchte nicht, dass mein Code schlecht oder überladen aussieht. Was wäre die eleganteste Lösung?

EDIT: Ich möchte darauf hinweisen, dass ich mehr an den allgemeinen guten Codierungspraktiken interessiert bin als an der Anwendung der spezifischen Funktion TreeMap. Wie einige der Antworten bereits gezeigt haben (und ich habe den ersten als beantwortet markiert), sollte die mögliche höhere Abstraktionsebene verwendet werden, ohne die Funktionalität zu verlieren.

Jimijazz
quelle
1
Verwenden Sie eine TreeMap, wenn Sie die Funktionen benötigen. Das war wahrscheinlich eine Designentscheidung aus einem bestimmten Grund, daher sollte es auch Teil der Implementierung sein.
@JordanReiter soll ich einfach eine neue Frage mit dem gleichen Inhalt hinzufügen, oder gibt es einen internen Cross-Posting-Mechanismus?
Jimijazz
2
"Ich verstehe die Vorteile der Initialisierungsrichtlinien, wie oben für große Programme beschrieben." Es ist nicht vorteilhaft, überall zu wirken, egal wie groß das Programm ist
Ben Aaronson,

Antworten:

23

"Programmieren auf eine Schnittstelle" bedeutet nicht "Verwenden Sie die am besten abstrahierte Version". In diesem Fall würde jeder nur verwenden Object.

Dies bedeutet, dass Sie Ihr Programm anhand der niedrigstmöglichen Abstraktion definieren sollten, ohne die Funktionalität zu verlieren . Wenn Sie a benötigen, müssen TreeMapSie einen Vertrag mit a definieren TreeMap.

Jeroen Vannevel
quelle
2
TreeMap ist keine Schnittstelle, sondern eine Implementierungsklasse. Es implementiert die Schnittstellen Map, SortedMap und NavigableMap. Die beschriebene Methode ist Teil der NavigableMap- Oberfläche. Die Verwendung von TreeMap würde verhindern, dass die Implementierung zu einer ConcurrentSkipListMap (zum Beispiel) wechselt, bei der es sich um den gesamten Punkt der Codierung auf einer Schnittstelle und nicht um die Implementierung handelt.
3
@MichaelT: Ich habe mir nicht die genaue Abstraktion angesehen, die er in diesem speziellen Szenario benötigt, also habe ich sie TreeMapals Beispiel verwendet. "Programm auf eine Schnittstelle" sollte nicht wörtlich als Schnittstelle oder abstrakte Klasse verstanden werden - eine Implementierung kann auch als Schnittstelle betrachtet werden.
Jeroen Vannevel
1
Obwohl die öffentliche Schnittstelle / Methoden einer Implementierungsklasse technisch gesehen eine 'Schnittstelle' ist, bricht sie das Konzept hinter LSP und verhindert das Ersetzen einer anderen Unterklasse, weshalb Sie public interfaceeher auf eine als auf 'die öffentlichen Methoden einer Implementierung' programmieren möchten. .
@JeroenVannevel Ich stimme zu, dass die Programmierung auf eine Schnittstelle erfolgen kann, wenn die Schnittstelle tatsächlich durch eine Klasse dargestellt wird. Ich sehe jedoch nicht, welchen Nutzen die Verwendung TreeMapgegenüber SortedMapoderNavigableMap
toniedzwiedz
16

Wenn Sie weiterhin eine Schnittstelle verwenden möchten, können Sie diese verwenden

NavigableMap <X, Y> map = new TreeMap<X, Y>();

Es ist nicht erforderlich, immer eine Schnittstelle zu verwenden, aber es gibt häufig einen Punkt, an dem Sie eine allgemeinere Ansicht einnehmen möchten, mit der Sie die Implementierung ersetzen können (möglicherweise zum Testen). Dies ist einfach, wenn alle Verweise auf das Objekt als das abstrahiert werden Oberflächentyp.


quelle
3

Der Grund für die Codierung in eine Schnittstelle und nicht in eine Implementierung besteht darin, zu vermeiden, dass Implementierungsdetails verloren gehen, die Ihr Programm sonst einschränken würden.

Betrachten Sie die Situation, in der die Originalversion Ihres Codes a verwendet HashMapund diese offengelegt hat.

private HashMap foo = new HashMap();
public HashMap getFoo() { return foo; }  // This is bad, don't do this.

Dies bedeutet, dass jede Änderung an getFoo()eine brechende API-Änderung darstellt und die Benutzer unglücklich machen würde. Wenn Sie nur garantieren, dass fooes sich um eine Karte handelt, sollten Sie diese stattdessen zurückgeben.

private Map foo = new HashMap();
public Map getFoo() { return foo; }

Dies gibt Ihnen die Flexibilität, die Funktionsweise Ihres Codes zu ändern. Sie erkennen, dass es sich footatsächlich um eine Karte handeln muss, die die Dinge in einer bestimmten Reihenfolge zurückgibt.

private NavigableMap foo = new TreeMap();
public Map getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

Und das bricht nichts für den Rest des Codes.

Sie können den Vertrag, den die Klasse gibt, später stärken, ohne etwas zu brechen.

private NavigableMap foo = new TreeMap();
public NavigableMap getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

Dies befasst sich mit dem Liskov-Substitutionsprinzip

Substituierbarkeit ist ein Prinzip in der objektorientierten Programmierung. Es heißt, dass in einem Computerprogramm, wenn S ein Subtyp von T ist, Objekte vom Typ T durch Objekte vom Typ S ersetzt werden können (dh Objekte vom Typ S können Objekte vom Typ T ersetzen), ohne eines der gewünschten zu ändern Eigenschaften dieses Programms (Korrektheit, ausgeführte Aufgabe usw.).

Da NavigableMap ein Untertyp von Map ist, kann diese Ersetzung ohne Änderung des Programms durchgeführt werden.

Das Offenlegen der Implementierungstypen macht es schwierig, die interne Funktionsweise des Programms zu ändern, wenn eine Änderung vorgenommen werden muss. Dies ist ein schmerzhafter Prozess und führt oft zu hässlichen Problemumgehungen, die dem Codierer später nur noch mehr Schmerzen zufügen (ich sehe Ihren vorherigen Codierer, der aus irgendeinem Grund ständig Daten zwischen einer LinkedHashMap und einer TreeMap gemischt hat - vertrauen Sie mir, wann immer ich es tue siehe deinen Namen in svn Schuld, ich mache mir Sorgen).

Sie möchten weiterhin vermeiden, dass Implementierungstypen verloren gehen. Beispielsweise möchten Sie möglicherweise ConcurrentSkipListMap stattdessen aufgrund einiger Leistungsmerkmale implementieren, oder Sie mögen nur die java.util.concurrent.ConcurrentSkipListMapund nicht java.util.TreeMapdie Importanweisungen oder was auch immer.


quelle
1

Stimmen Sie den anderen Antworten zu, dass Sie die allgemeinste Klasse (oder Schnittstelle) verwenden sollten, von der Sie tatsächlich etwas benötigen, in diesem Fall TreeMap (oder wie jemand NavigableMap vorgeschlagen hat). Aber ich möchte hinzufügen, dass dies mit Sicherheit besser ist, als überall darauf zu werfen, was auf jeden Fall ein viel größerer Geruch wäre. Siehe /programming/4167304/why-should-casting-be-avoided aus bestimmten Gründen.

Sebastiaan van den Broek
quelle
1

Es geht um Ihre Kommunikation Absicht der , wie sollte das Objekt verwendet werden. Wenn Ihre Methode beispielsweise ein MapObjekt mit einer vorhersagbaren Iterationsreihenfolge erwartet :

private Map<String, String> processOrderedMap(LinkedHashMap<String, String> input) {
    // ...
}

Wenn Sie den Aufrufern der oben genannten Methode unbedingt mitteilen müssen, dass auch sie ein MapObjekt mit einer vorhersagbaren Iterationsreihenfolge zurückgeben , da aus irgendeinem Grund eine solche Erwartung besteht:

private LinkedHashMap<String,String> processOrderedMap(LinkedHashMap<String,String> input) {
    // ...
}

Natürlich können Aufrufer das Rückgabeobjekt weiterhin Mapals solches behandeln, aber das geht über den Rahmen Ihrer Methode hinaus:

private Map<String, String> output = processOrderedMap(input);

Einen Schritt zurück machen

Der allgemeine Ratschlag zum Codieren einer Schnittstelle ist (allgemein) anwendbar, da normalerweise die Schnittstelle die Garantie dafür bietet, was das Objekt ausführen kann, auch bekannt als Vertrag . Viele Anfänger beginnen mit HashMap<K, V> map = new HashMap<>()und es wird empfohlen, dies als a zu deklarieren Map, da a HashMapnicht mehr bietet als das, was a Maptun soll. Daraus können sie dann (hoffentlich) verstehen, warum ihre Methoden a Mapanstelle von a aufnehmen sollten HashMap, und so können sie die Vererbungsfunktion in OOP realisieren.

Um nur eine Zeile aus dem Wikipedia-Eintrag des Lieblingsprinzips aller zu diesem Thema zu zitieren :

Es ist eher eine semantische als nur eine syntaktische Beziehung, da es die semantische Interoperabilität von Typen in einer Hierarchie gewährleisten soll ...

Mit anderen Worten, die Verwendung einer MapDeklaration liegt nicht daran, dass sie "syntaktisch" sinnvoll ist, sondern Aufrufe des Objekts sollten sich nur darum kümmern, dass es sich um eine Art von Deklaration handelt Map.

Sauberer Code

Ich finde, dass ich dadurch manchmal auch saubereren Code schreiben kann, insbesondere wenn es um Unit-Tests geht. Das Erstellen eines HashMapmit einem einzigen schreibgeschützten Testeintrags dauert mehr als eine Zeile (ohne die Verwendung der doppelten Klammerinitialisierung), wenn ich dies leicht durch ersetzen kann Collections.singletonMap().

hjk
quelle