Nun, die Vor- und Nachteile sind weitgehend klar. Im wesentlichen: 1. DistanceFunction
ist ein schönerer Name mit schönerer Signatur, und 2. ToDoubleBiFunction
ist allgemeiner und Teil der Standard-API. Es geht vielleicht („nur“?) um die Gewichtung…
Angnommen, ich habe eine Lib, die, wie oben schon angedeutet, z.B. ein „Datensatz“-Objekt anbietet:
interface DataSet<T> {
DistanceFunction<T> getDistanceFunction();
List<T> getData();
}
Diese Lib hat an sich die Abhängigkeit zu der Lib, wo DistanceFunction
definiert ist. Dann könnte man sagen: Alles klar, die Abhängigkeit ist da, und das Interface mit dem schönen, sprechenden Namen ist da, als verwendet man es auch.
Jetzt gibt es aber eine weitere Lib. Die hat mit der DistanceFunction
-Lib eigentlich nichts zu tun. Sie bietet nur eine Funktion an, die aus einer Liste von Objekten und einer Distanzfunktion eine Distanzmatrix berechnet:
double[][] computeDistanceMatrix(
List<T> objects,
DistanceFunction<? super T> distanceFunction)
{
....
}
Es macht eigentlich keinen Sinn, dort die Abhängigkeit zur DistanceFunction
-Lib einzubauen, weil die Funktion, die benötigt wird, besser als ToDoubleBiFunction
gegeben sein sollte.
So gesehen bedeutet das aber, dass
-
DistanceFunction
dann ToDoubleBiFunction
erweitern müßte (nicht schlimm - so ist es im Moment) ODER
- Jeder, der einen
DataSet
bekommt, die DistanceFunction
selbst (per Lambda) als ToDoubleBiFunction
an diese Funktion weiterreichen müßte:
computeDistanceMatrix(dataSet.getData(),
(d, d) -> dataSet.getDistanceFunction().distance(d,d)); // = ToDoubleBiFunction
Dann stellt sich aber die Frage, ob DistanceFunction
überhaupt noch eine Existenzberechtigung hat.
Natürlich könnte man die Frage verallgemeinern. In letzter Konsequenz wäre die Frage: „Sollte es mehrere strukturell gleiche Interfaces mit unterschiedlichen Namen geben?“. Ich weiß nicht, ob jemand auf die Idee kommen würde, aus jedem ActionListener
nun einen Consumer<ActionEvent>
zu machen - aber objektiv betrachtet wäre das nicht in jeder Hinsicht „schlecht“ … … …
Ein Argument für das DistanceFunction
interface wäre, dass man es ggf. um Dinge erweitern könnte, die spezifisch für Distanzfunktionen sind…
interface DistanceFunction<T> {
double distance(T t0, T t1);
// For Euclidean distances, this is implemented
// differently, to avoid the expensive square root
default double distanceSquared(T t0, T t1) {
double d = distance(t0, t1);
return d*d;
}
}
womit die Lösung mit dem ToDoubleBinaryOperator
auch wegfallen würde. (Auch da stellt sich die Frage: „Lohnt“ sich das, als eigene Klasse, die eine Abhängigkeit verursacht, wenn es doch schon mit einem existierenden Interface abgebildet werden kann…?)
EDIT @SlaterB Zeitliche Überschneidung, Update:
Das Beispiel mit dem Comparator
hat ja gewisse Ähnlichkeit zu meinem mit dem ActionListener
. Man könnte die Frage, wie schon angedeutet, seht weit verallgemeinern. Und schon in anderen Zusammenhängen hatte ich angedeutet, dass man, wenn man es darauf anlegt, 99% seiner Programme mit ein paar Set
, Tuple
und Function
modellieren könnte (oder, um die LISP-Jünger zufrieden zu stellen (oder zu bashen) : 100% seiner Programme mit ein paar Listen :D).
Diese Generizität (an Funktionale Programmierung allgemein angelehnt) hat viele theoretische Vorteile, und auch praktische in bezug auf die Flexibilität. Trotzdem gibt es nunmal eine Sache, die Objektorienterte Programmierung ausmacht - die sie ist (oder „im innersten Zusammenhält“). Und das sind Namen. Sprechende Namen für die Dinge, die man da modelliert. Und diese Namen sind (dadurch, dass sie einem helfen, in Strukturen zu denken und damit auch komplexe Systeme hanhabbar zu halten) so gesehen der Erfolgsgrund der OOP.
Das ganze Problem würde schon dramatisch abgeschwächt, wenn es sowas wie typedef
gäbe (aber nicht das aus C, sondern ein vernünftiges). Auf Java übertragen würde das bedeuten, dass der Compiler erkennen könnte (und sollte - ich frage mich, warum er das nicht tut…
) dass zwei Interfaces eben strukturell gleich sind, bzw. das eine durch das andere abgebildet werden kann.
Sowas wie
<R> R collect(... Accumulator<R,? super T> accumulator, ...)
wäre ja durchaus nett und schön, wenn man es mit
BiConsumer<X,Y> myAccumulator = ...;
collect(... myAccumulator...);
aufrufen könnte - der Compiler könnte durchaus feststellen, dass er dafür „nur“ die trivial-Mechanische Umwandlung machen müßte, die ich oben schon für die DistanceFunction
angedeutet hatte:
BiConsumer<X,Y> myAccumulator = ...;
Accumulator<X,Y> trivialAndInsertedByTheCompiler = (x,y) -> myAccumulator.accept(x,y);
collect(... trivialAndInsertedByTheCompiler ...);
(sicher, in anderen Fällen wäre das komplizierter, aber so spontan fragt man sich schon, warum er das nicht schaffen sollte…)
Die „typtheoretischen“ Überlegungen (und leichten Abschweifungen zu Compilerbau und Typinferenz) mal etwas außen vor gelassen: Ein wichtiger Punkt der Frage war die Abhängigkeit. Sollte man
interface DataSet<T> {
DistanceFunction<T> getDistanceFunction();
List<T> getData();
}
schreiben, und damit jeden, der das verwenden will, zur Verwendung der „lästigen“ (und so erstmal nicht wirklich notwendigen (und unmittelbar nicht mal nutzbringenden)) DistanceFunction
-Lib nötigen, oder auf das allgemeine, Abhängigkeitsfreie
interface DataSet<T> {
ToDoubleBiFunction<T, T> getDistanceFunction();
List<T> getData();
}
zurückgreifen?