Ist es empfehlenswert, zwei Java 8-Standardmethoden in Bezug aufeinander zu implementieren?

14

Ich entwerfe eine Schnittstelle mit zwei ähnlichen Methoden:

public interface ThingComputer {
    default Thing computeFirstThing() {
        return computeAllThings().get(0);
    }

    default List<Thing> computeAllThings() {
        return ImmutableList.of(computeFirstThing());
    }
}

Etwa die Hälfte der Implementierungen berechnet immer nur eine Sache, während die andere Hälfte möglicherweise mehr berechnet.

Hat dies einen Präzedenzfall in weit verbreitetem Java 8-Code? Ich weiß, dass Haskell in einigen Schriftklassen ( Eqzum Beispiel) ähnliche Dinge tut .

Der Vorteil ist, dass ich deutlich weniger Code schreiben muss als mit zwei abstrakten Klassen ( SingleThingComputerund MultipleThingComputer).

Der Nachteil ist, dass eine leere Implementierung kompiliert wird, aber zur Laufzeit mit einem explodiert StackOverflowError. Es ist möglich, die gegenseitige Rekursion mit einem zu erkennen ThreadLocalund einen schöneren Fehler zu melden, aber dies erhöht den Aufwand für nicht fehlerhaften Code.

Tavian Barnes
quelle
3
Warum würden Sie jemals absichtlich Code schreiben, der garantiert eine unendliche Rekursion hat? Daraus kann möglicherweise nichts Gutes entstehen.
1
Nun, es ist nicht "garantiert"; Eine ordnungsgemäße Implementierung würde eine dieser Methoden außer Kraft setzen.
Tavian Barnes
6
Sie wissen nicht, dass. Eine Klasse oder Schnittstelle muss für sich allein stehen und darf nicht davon ausgehen, dass eine Unterklasse bestimmte Maßnahmen ergreift, um schwerwiegende Logikfehler zu vermeiden. Ansonsten ist es spröde und kann seine Garantien nicht einhalten. Beachten Sie, dass ich nicht sage, dass Ihre Schnittstelle eine implementierende Klasse durchsetzen kann oder sollte throw new Error();oder etwas Dummes, nur dass die Schnittstelle selbst keinen spröden Vertrag über defaultMethoden haben sollte.
Ich stimme @Snowman zu, es ist eine Landmine. Ich denke, es ist Lehrbuch "böser Code" in dem Sinne, dass Jon Skeet bedeutet - beachten Sie, dass das Böse nicht dasselbe ist wie das Böse , es ist böse / klug / verlockend, aber immer noch falsch :) Ich empfehle youtu.be/lGbQiguuUGc?t=15m26s ( oder in der Tat, das gesamte Gespräch für einen breiteren Kontext - es ist sehr interessant).
Konrad Morawski,
1
Nur weil jede Funktion in Bezug auf die andere definiert werden kann, bedeutet dies nicht , dass beide Funktionen in Bezug aufeinander definiert werden können. Das fliegt in keiner Sprache. Sie möchten eine Schnittstelle mit 2 abstrakten Vererbern, die jeweils eine in Bezug auf die andere implementieren, die andere jedoch abstrahieren, sodass die Implementierer auswählen, welche Seite sie implementieren möchten, indem sie von der richtigen abstrakten Klasse erben. Eine Seite muss unabhängig von der anderen definiert werden, sonst geht Pop über den Stapel . Sie haben irrsinnige Standardeinstellungen, bei denen davon ausgegangen wird, dass Implementierer das Problem für Sie lösen abstract, um sie zu einer Lösung zu zwingen .
Jimmy Hoffa

Antworten:

16

TL; DR: Tu das nicht.

Was Sie hier zeigen, ist spröder Code.

Eine Schnittstelle ist ein Vertrag. Es heißt: "Unabhängig davon, welches Objekt Sie erhalten, kann es X und Y ausführen." Wie bereits geschrieben, funktioniert Ihre Schnittstelle weder mit X noch mit Y, da garantiert wird , dass ein Stapelüberlauf auftritt.

Das Auslösen eines Fehlers oder einer Unterklasse weist auf einen schwerwiegenden Fehler hin, der nicht abgefangen werden sollte:

Ein Fehler ist eine Unterklasse von Throwable, die auf schwerwiegende Probleme hinweist, die eine vernünftige Anwendung nicht abfangen sollte.

Darüber hinaus VirtualMachineError , die Elternklasse von Stackoverflow , sagt dieser:

Wird ausgelöst, um anzuzeigen, dass die Java Virtual Machine defekt ist oder nicht mehr genügend Ressourcen zur Verfügung stehen, um den Betrieb fortzusetzen.

Ihr Programm sollte sich nicht mit JVM-Ressourcen befassen . Das ist die Aufgabe der JVM. Das Erstellen eines Programms, das im Rahmen des normalen Betriebs einen JVM-Fehler verursacht, ist fehlerhaft. Entweder wird garantiert, dass Ihr Programm abstürzt, oder die Benutzer dieser Schnittstelle werden gezwungen, Fehler abzufangen, mit denen sie sich nicht befassen sollten.


Vielleicht kennen Sie Eric Lippert von solchen Bemühungen als emeritiertes "Mitglied des C # -Sprachdesign-Komitees". Er spricht über Sprachmerkmale, die Menschen zum Erfolg oder Misserfolg bewegen: Während dies kein Sprachmerkmal oder Teil des Sprachdesigns ist, gilt sein Standpunkt ebenso für die Implementierung von Schnittstellen oder die Verwendung von Objekten.

Erinnern Sie sich an The Princess Bride, als Westley aufwacht und mit einem heiseren Albino und dem finsteren Sechsfinger-Mann, Graf Rugen, in The Pit Of Despair gefangen ist? Die Grundidee einer Grube der Verzweiflung ist zweifach. Erstens, dass es eine Grube ist und daher leicht zu fallen ist, aber schwierig zu überwinden ist. Und zweitens, dass es Verzweiflung auslöst. Daher der Name.

Quelle: C ++ und die Grube der Verzweiflung

Wenn ein Interface StackOverflowErrorstandardmäßig einen wirft, werden die Entwickler in die Grube der Verzweiflung gedrängt und es ist eine schlechte Idee . Schieben Sie die Entwickler stattdessen in Richtung Pit of Success . Machen Sie es einfach Ihre Schnittstelle richtig zu verwenden und ohne die JVM abstürzt.

Das Delegieren zwischen den Methoden ist hier in Ordnung. Die Abhängigkeit sollte jedoch in eine Richtung gehen. Ich stelle mir Methodendelegation gerne als gerichteten Graphen vor . Jede Methode ist ein Knoten im Diagramm. Zeichnen Sie jedes Mal, wenn eine Methode eine andere Methode aufruft, eine Kante von der aufrufenden Methode zu der aufgerufenen Methode.

Wenn Sie ein Diagramm zeichnen und feststellen, dass es zyklisch ist, ist das ein Codegeruch. Das ist ein Potenzial, um Entwickler in die Grube der Verzweiflung zu drängen. Beachten Sie, dass es nicht grundsätzlich verboten sein sollte, nur dass man Vorsicht walten lassen muss . Rekursive Algorithmen haben speziell Zyklen im Aufrufdiagramm: das ist in Ordnung. Dokumentieren Sie es und warnen Sie die Entwickler. Wenn es nicht rekursiv ist, versuchen Sie, diesen Zyklus zu unterbrechen. Wenn dies nicht möglich ist, ermitteln Sie, welche Eingaben einen Stapelüberlauf verursachen können, und verringern Sie diese oder dokumentieren Sie sie als letzten Fall, wenn sonst nichts funktioniert.

Gemeinschaft
quelle
Gute Antwort. Ich bin gespannt, warum das in Haskell so üblich ist. Ich denke, verschiedene Dev-Kulturen.
Tavian Barnes
Ich habe keine Erfahrung in Haskell und kann nicht darüber sprechen, ob oder warum dies in der funktionalen Programmierung häufiger vorkommt.
Wenn man es sich etwas genauer anschaut, scheint es, dass die Haskell-Community auch nicht wirklich glücklich damit ist. Außerdem hat der Compiler kürzlich gelernt, nach "minimalen vollständigen Definitionen" zu suchen, sodass eine leere Implementierung zumindest eine Warnung generiert.
Tavian Barnes
1
Die meisten funktionalen Programmiersprachen verfügen über eine Tail Call-Optimierung. Mit dieser Optimierung würde die Schwanzrekursion den Stapel nicht sprengen, daher sind solche Methoden sinnvoll.
Jason Yeo