Welchen Vorteil bringt mir die Referenzierung über die Superklasse oder das Interface?
Der Vorteil ist, dass Dein Code dann deutlich universeller ist. Anpassungen sind deutlich einfacher.
Schauen wir uns einfach einmal ein paar Beispiele an.
a) konkrete Instanz überall nutzen statt einem Interface / einer Superklasse
Stell Dir vor, Du hast diverse Methoden, die etwas mit Listen machen. Da Du immer java.util.ArrayList verwendest, hast Du das auch in den Methoden verwendet - statt eben dem Interface java.util.List. Das funktioniert natürlich erst einmal super.
a1: Nun hast Du aber aus irgend einem Grund eine andere Implementation von List. Du (oder jemand anderes) hast an irgend einer Stelle java.util.LinkedList verwendet. Oder Du willst die Mehode java.util.Array.asList(E... ) verwenden - etwas, das zwar eine ArrayList zurück gibt, aber eben kein java.util.ArrayList sondern java.util.Arrays$ArrayList. Die Methoden von Dir sind nicht verwendbar, wenn diese als Parameter ein java.util.ArrayList erwarten. Du schränkst also die Nutzbarkeit Deiner Methoden massiv ein - und das ohne jeden Grund (so du nur die Methoden von dem Interface verwendest).
a2: Nun stellst Du fest: Die Klasse ArrayList erfüllt nicht die Anforderungen, die Du an diese hast. Daher willst Du zukünftig eine andere List verwenden. Es reicht aber nun nicht, dass Du die Stelle anpasst, an der Du die Instanz erzeugst. Jetzt musst Du auch alle Stellen anpassen, bei denen Du es genutzt hast. Hier merkst Du also, dass es auch massive Konsequenzen hat, wenn Du Deinen Code nicht anderweitig verwenden können willst sondern nur für Dein Projekt nutzen willst.
b) Die Fragestellung ist auch weiter zu sehen. Du hast eine Vererbungshierarchie auch bei Interfaces. Nehmen wir uns nur einmal java.util.List:
Iterable -> Collection -> List
Die Argumente oben bei a1 gelten hier auch. Wenn Du in einer Methode bei einem Parameter nur Dinge verwendest, die Iterable oder Collection auch haben, dann macht es Sinn, bei den Argumenten den Typ entsprechend anzupassen. Wenn Du statt List den Typ Collection verwendest, dann kannst Du nicht nur beliebige List Implementationen übergeben sondern auch z.B. Sets. Deine Methode ist also deutlich besser verwendbar.
c) Bezüglich Interfaces ist evtl. auch noch wichtig:
Versuche, Interfaces möglichst klein zu halten (wenn Du welche erstellst). Du hast also nicht ein großes, komplexes Interface sondern lieber mehrere. Also statt einem Interface:
Hund, der fressen(), bellen(), laufen(), schlafen(), .... kann erstellst du lieber mehrere Interfaces:
KannFressen, KannBellen, KannLaufen, KannSchlafen, ....
Damit bist Du dann deutlich universeller. Das Interface KannBellen kann auch eine Alarmanlage haben, die halt ein Hundegebell imitiert. fressen können auch Katzen, Pferde, ... Und schlafen() kann auch meine CPU (Sleep Modus)
Das ist nicht mehr ganz Deine Fragestellung, aber ich denke es ist so nah dran, dass ich es noch Sinn macht, diesen Punkt auch zu erwähnen.
Und das kann man dann auch etwas einordnen: Wie wichtig ist das denn?
Es ist aus meiner Sicht existenziell, dass man sich an sowas hält. Das ist auch durch SOLID Prinzipien (
Prinzipien objektorientierten Designs – Wikipedia) abgedeckt, was ich als notwendige Basis für Clean Code betrachten würde.
Wie sieht denn dann die Praxis aus?
In der Praxis macht es Sinn, bei Klassen, die man verwenden will, auch immer die Interfaces anzusehen und kennen zu lernen. Dann kann man überlegen, was man überhaupt braucht um dann explizit das Interface zu nutzen um eben möglichst offen für zukünftige Verwendungen zu sein. (a1 / a2 aus dem Post!)
Wichtig ist aber: Daraus folgt nicht, dass man selbst massiv Interfaces erstellen muss. Das macht nur dann Sinn, wenn absehbar ist, dass es weitere Implementationen geben könnte. Und bei Methoden macht dies nur Sinn, wenn man meint, dass die Methode auch für andere Klassen verwendbar sein könnte.
Wichtig ist dabei, dass Interfaces immer vom Verbraucher angefordert werden, nicht von dem, der diese bereit stellt. Wenn ich den Hund implementiere, dann weiss ich nicht, was Andere brauchen. Dass fressen() und saufen() als Interface Fuetterbar notwendig sind, merke ich erst, wenn ich die FutterStation implementiere. Und dann ist klar: Die FutterStation kann universell verwendet werden und nicht nur für Hunde. => Der Bedarf für Fuetterbar ist aufgekommen.
Und das zeigt auch: Vieles entsteht während der Entwicklung. Man schreibt erst einmal Code und stellt dann im Laufe der Entwicklung fest: Ich habe da etwas geschrieben, das nicht optimal ist. Und dann passt man es an. Dann kann man nachträglich Interfaces einfügen und so.
(Bei der Entwicklung von Libraries ist dies natürlich erschwert. Da muss man beim Design der Library über mögliche Verwendungen nachdenken und das macht sowas deutlich schwerer. Aber unter dem Strich läuft es da auch nicht anders.)
==> Der erste Schritt ist aber auf jeden Fall, die Klassen zu kennen, die man nutzt. Und dazu gehören auch die Interfaces der Klassen. Und dann kann man das Vorhandene direkt nutzen.
==> Der Zweite Schritt sind dann erst einmal Refactorings. Da muss man aber langsam rein kommen. Hier ist meine Empfehlung:
www.clean-code-developer.de - da wird man Schrittweise über mehrere Grade in diese Themen eingeführt.