Einige Programmiersprachen bieten ein Feature mit dem etwas eigenartigen Namen “Ducktyping”. Java zählt leider nicht dazu. In diesem Artikel möchte ich kurz erklären, was Ducktyping ist und wie man mit den in Java 8 eingeführten Methodenreferenzen zumindest einen ähnlichen Effekt erzielen kann.
Am bekanntesten ist das Konzept von Ducktyping vermutlich aus JavaScript. Der Begriff “Ducktyping” geht wohl auf ein Gedicht des amerikanischen Schriftstellers James Whitcomb Riley zurück, worin es heißt:
“When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.”
“Wenn ich einen Vogel sehe, der läuft wie eine Ente und schwimmt wie eine Ente und schnattert wie eine Ente, dann nenne ich diesen Vogel auch Ente.”
Wie passt das mit Programmierung zusammen? Stellen wir uns vor wir haben eine Funktion, die ein Enten-Objekt als Parameter erwartet. In der Funktion wollen wir die “schnattern”-Methode der Ente aufrufen. Ob es sich bei dem übergebenen Objekt aber wirklich um eine Ente handelt, kann uns in vielen Fällen eigentlich herzlich egal sein – Hauptsache es besitzt eine Methode “schnattern” mit der passenden Signatur. Bei Ducktyping wird also die Frage “Ist es eine Ente?” nicht am tatsächlichen Typ des Objekts festgemacht, sondern an den Eigenschaften, die das Objekt besitzt.
Ein anderes, realeres Beispiel ist die AJAX-Funktion von JQuery. Diese erwartet als Argument ein Objekt, welches einen url-Wert und einen success-Callback enthält (Stark vereinfacht. Tatsächlich gibt es noch zahlreiche weitere Varianten bei JQuery).
Java unterstützt kein Ducktyping. In der Methodensignatur muss für jedes Argument der richtige Typ angegeben werden. Ein Argument muss also beispielsweise das “Ente”-Interface implementieren. Es reicht nicht, dass eine Klasse die gleichen Methoden besitzt, die im Interface vorgesehen sind, nein es muss auch wirklich implements Ente geschrieben werden. Folgendes Beispiel, in Anlehnung an das JQuery-AJAX-Beispiel, soll das verdeutlichen:
public interface Request {
String getUrl();
void callback(String result);
}
public class MyRequest implements Request {
public String getUrl() {
return "http://blog.saxsys.de";
}
public void callback(String result) {
System.out.println(result);
}
}
public void ajax(Request request) {
...
}
Ich habe ein Interface Request definiert und eine implementierende Klasse MyRequest. Die Methode getUrl gibt die Ziel-Adresse zurück und die callback-Methode wird von unserem AJAX-Framework aufgerufen, sobald die Antwort eingetroffen ist. Da die ajax-Methode ein Argument vom Typ Request verlangt, kann ich ohne Probleme eine Instanz von MyRequest benutzen. Habe ich aber beispielsweise folgende Klasse, sieht die Sache ganz anders aus:
public class OtherRequest {
public getUrl(){
return "http://stage-sgs.dsinet.de";
}
public void callback(String result) {
System.out.println(result);
}
}
Obwohl diese Klasse ebenfalls die beiden benötigten Methoden mit der identischen Signatur besitzt, würde die ajax-Methode eine Instanz dieser Klasse verweigern, da die Klasse das nicht das Interface Request implementiert. Das kann oft ärgerlich sein, vor allem, wenn man eine gegebene Klasse nicht einfach selbst verändern kann, also nicht einfach implements Request dran schreiben kann, beispielsweise wenn diese aus einer Drittbibliothek stammt.
Ad-Hoc-Implementation von Interfaces
Bei anderen Programmiersprachen ist das zum Teil anders: Bei Haskell beispielsweise gibt es zwar keine “Interfaces” im Java-Sinne aber man könnte Haskells “type classes” in etwa mit Interfaces vergleichen. Und hier kann man Typen auch unabhängig von der Typdefinition nachträglich zu beliebigen Type-Classes hinzufügen. Ich könnte also meinen Typ OtherRequest der Type-Class Request hinzufügen, ohne dass ich den Quellcode von OtherRequest anfassen muss. Anschließend kann ich OtherRequest-Werte als Parameter für Funktionen nutzen, die Request als Typ verlangen.
Schauen wir uns ein Beispiel an:
class Request r where
getUrl :: r -> String
ajax :: (Request r) => r -> IO ()
ajax r = do print $ getUrl r
Hier wird eine Type-Class Request mit einer Funktion getUrl definiert, die einen konkreten Request entgegen nimmt und einen String zurück gibt. Jeder Datentyp, der dieser Type-Class hinzugefügt wird muss also eine solche Funktion bereitstellen. Darunter befindet sich die ajax-Funktion, die einen Request entgegen nimmt und eine IO-Aktion zurück gibt. Die hier gezeigte Implementierung ist nur ein Platzhalter und führt nicht wirklich einen AJAX-Request aus, sondern gibt lediglich die URL auf der Kommandozeile aus. Dazu kann es aber auf die getUrl Funktion zurück greifen, da diese ja in der Type-Class definiert wurde. Dies entspricht also unserem AJAX-Library-Code.
In unserem Programm könnten wir nun unseren eigenen Datentyp für unseren Request definieren. Um das AJAX-Framework benutzen zu können müssen wir unseren Datentyp zur “Request”-Type-Class hinzufügen:
data OtherRequest = OtherRequest {url::String} deriving (Show)
instance Request OtherRequest where
getUrl = url
x = OtherRequest "http://www.saxsys.de"
ajax x
Mit dem data-Schlüsselwort wird in Haskell ein neuer Typ definiert, der in unserem Fall “OtherRequest” heißt und einen String “url” besitzt. Haskell erzeugt damit eine Funktion “url”, mit der Signatur OtherRequest -> String, die also ein OtherRequest-Wert entgegen nimmt und einen String zurück gibt.
Mit instance wird unser Typ der Type-Class hinzugefügt. Mit der Zeile “getUrl = url” sagen wir aus, dass die “url”-Funktion unseres Typs benutzt werden soll, wenn “getUrl” von der Type-Class aufgerufen wird. Wir könnten an der Stelle aber auch eine andere Implementierung angeben. Dies ist also vergleichbar mit der Situation in Java, wo in einer Klasse eine Methode implementiert wird, die durch ein Interface vorgeschrieben wird.
Der untere Teil des Codeausschnitts zeigt, wie wir das ganze benutzen können. Wir erstellen einen “OtherRequest”-Wert und weisen ihm den Namen “x” zu. Anschließend wird die “ajax”-Funktion mit x als Argument aufgerufen. Da wir “OtherRequest” der Type-Class “Request” hinzugefügt haben, funktioniert dieser Aufruf wie erwartet.
Da Haskell keine objektorientierte Sprache ist, lässt sich das ganze nicht komplett mit Java vergleichen. Der springende Punkt ist aber: Die Definition unseres Typs und das Hinzufügen zur Type-Class (also das Implementieren des Interfaces) sind zwei unabhängige Vorgänge, die an unabhängigen Stellen im Code durchgeführt werden können. Der Typ “OtherRequest” hätte auch aus einer Dritt-Bibliothek stammen können. Trotzdem könnten wir ihn für unser Programm der benötigten Type-Class hinzufügen um ihn benutzen zu können. In Punkto Erweiterbarkeit ist diese Variante also ein guter Ersatz für Ducktyping, vorallem da sogar von der konkreten Benennung abstrahiert werden kann (siehe “getUrl = url”).
Dynamische oder statische Typisierung
Ducktyping wird in der Regel vor allem im Zusammenhang mit dynamisch Typisierten Sprachen wie JavaScript genannt. Bei diesen wird also zur Laufzeit bestimmt, ob ein Objekt die nötigen Methoden und Attribute besitzt. Aber auch bei statischer Typisierung ist Ducktyping möglich. Die Prüfung, ob ein Objekt “eine Ente ist”, findet dann zur Compile-Zeit statt. Ein gutes Beispiel ist Typescript. Unser Request-Beispiel von vorhin könnte in Typescript so aussehen:
interface Request {
getUrl(): string
callback(result: String)
}
class MyRequest implements Request {
getUrl() {
return "http://example.org";
}
callback(result: String) {
console.log(result);
}
}
class OtherRequest {
getUrl() {
return "http://example.org";
}
callback(result: String) {
console.log(result);
}
}
function ajax(request: Request) {
}
var r1 = new MyRequest();
var r2 = new OtherRequest();
ajax(r1);
ajax(r2);
Die ajax-Funktion erwartet ein Argument vom Interface-Typ Request. Aber auch eine Instanz von OtherRequest wird vom Compiler akzeptiert, obwohl diese Klasse das Interface nicht explizit implementiert – es reicht, dass alle Methoden mit der korrekten Signatur vorhanden sind. Der Code kann hier auch live ausprobiert werden.
Zurück zu Java. Wie oben schon erwähnt, unterstützt Java kein Ducktyping. Mit den Methoden-Referenzen aus Java 8 kann man aber einen ähnlichen Effekt erzielen, der zwar nicht so elegant, in einigen Situationen aber trotzdem sehr nützlich sein kann. Vor allem dann, wenn die zu benutzende Klasse eben nicht verändert werden kann oder soll.
Die Idee ist, einer Funktion nicht nur das Objekt mitzugeben, sondern auch mitzuteilen, wie die Funktion an die notwendigen Methoden des Objekts herankommen kann. Schauen wir uns das wieder am AJAX-Beispiel an:
public void ajax(Request request) {
String url = request.getUrl();
...
String result = ...
request.callback(result);
}
public <T> void ajax(T request, Function<T,String> urlExtractor, BiConsumer<T, String> callback) {
String url = urlExtractor.apply(request);
...
String result = ...;
callback.accept(request, result);
}
Wer bisher noch wenig mit den Funktionalen Klassen und Interfaces von Java 8 zutun hatte, für den sieht die Signatur der zweiten überladenen ajax-Methode vielleicht etwas gewöhnungsbedürftig aus. Zunächst definiert die Methode einen generischen Typ-Parameter T ohne dabei irgendwelche Typ-Grenzen festzulegen. Es kann folglich jede beliebige Instanz als erstes Argument übergeben werden, solange sichergestellt ist, dass die Funktionen, die als zweites und drittes Argument übergeben werden, zu diesem Typ passen. Das zweite Argument ist eine Funktion, die für ein gegebenes Objekt vom Typ T einen String (nämlich die URL) zurück liefert. Als drittes wird ein BiConsumer übergeben, sprich eine Funktion, die zwei Argumente (wieder das Request-Objekt und den Ergebnis-String) entgegen nimmt und keinen Rückgabewert hat. Mit Lambdas kann diese Funktion nun so aufgerufen werden:
MyOtherRequest myRequest = new MyOtherRequest();
ajax(myRequest, req -> req.getUrl(), (req, result) - req.callback(result));
Besser lesbar wird es mit Methodenreferenzen:
OtherRequest otherRequest = new OtherRequest();
ajax(myRequest, OtherRequest::getUrl, OtherRequest::callback);
Die Zeile liest sich quasi so: Nimm dieses Request-Objekt. Um an die URL zu gelangen, rufe die getUrl-Methode auf dem Request-Objekt auf. Und zur Verarbeitung des Ergebnisses bitte die callback-Methode auf dem Objekt aufrufen. Dieses Vorgehen ist zwar kein Ducktyping. Aber man kann es in Situationen nutzen, in denen man sich sonst Ducktyping wünschen würde. Die Methodensignatur drückt gewissermaßen Anforderungen an ein gegebenes Objekt aus, die von dem Objekt unterstützt werden müssen. Interessant dabei ist auch, dass die tatsächliche Benennung der Methoden bei dieser Variante egal ist. OtherRequest hätte seine Callback-Methode auch “rufMichAn_Sofort” nennen können. Übrigens hätte man im konkreten Fall die Signatur von ajax noch anders gestalten können:
public void ajax(Supplier<String> urlSupplier, Consumer<String> callback) {
String url = urlSupplier.get();
...
callback.accept("das Result");
}
// Aufruf
ajax(otherRequest::getUrl, otherRequest::callback);
Hier wird direkt eine Funktion zum Beschaffen der URL und ein Funktion zum Verarbeiten des Ergebnisses übergeben. Aus Sicht der ajax-Funktion spielt es keine Rolle, ob es da ein Request-Objekt gibt und von welchem Typ es ist. Wichtig ist der Unterschied beim Aufruf der Methode: Die Methodenreferenzen beziehen sich hier auf die Instanz otherRequest (mit kleinem Anfangsbuchstaben), während im oberen Beispiel die Methodenreferenzen auf die Klasse abzielten (OtherRequest mit großem Anfangsbuchstaben). Aus API-Design-Gesichtspunkten ist diese Variante am besten, da die ajax-Methode die wenigsten Annahmen über den Aufrufer anstellt und damit sehr gut komponierbar ist. Es gibt aber Situationen, wo die oben gezeigte “Quasi-Ducktyping”-Variante notwendig ist und man wirklich Objekte als Parameter entgegen nehmen möchte. Beispielsweise haben wir bei der Entwicklung des Model-Wrappers, der Teil von mvvmFX ist, diese Variante benutzt um Getter- und Setter-Methoden eines gewrappten Objekts bekannt zu machen.
Fazit
Die Frage, ob Java als Sprache Ducktyping direkt unterstützen sollte, ähnlich beispielsweise zu der oben gezeigten TypeScript-Variante, ist aber umstritten. Denn nur weil eine Klasse die gleichen Methodensignaturen besitzt ist ja noch lange nicht klar, ob es auch wirklich kompatibel oder fachlich passend ist. Mein Eindruck ist, dass viele Java-Entwickler ohnehin eher defensiv eingestellt sind, was man z.B. an der Debatte um “final” bei Methoden und Klassen sieht. Daher scheint es mir unwahrscheinlich, dass Java in nächster Zukunft echtes Ducktyping bekommen wird. Auch die Möglichkeit der Ad-Hoc-Implementierung von Interfaces, ähnlich wie es oben für Haskell gezeigt wurde, scheint wohl nicht auf der Liste der Erweiterungen zu stehen, die in nächster Zeit für die Java-Sprache zu erwarten sind.
Als Fazit lässt sich aber sagen, dass die funktionalen Interfaces von Java 8 hier zumindest einige Erleichterung bringen. Statt bestimmte eingeschränkte Typen für Methoden-Argumente vorschreiben zu müssen, können so beliebige Argumente verwendet werden, wenn mittels Funktionen mitgeteilt wird, wie diese Argumente zu interpretieren sind. Dies erleichtert die Integration von Fremdcode und vermindert Kopplung. Und mittels Methodenreferenzen kann eine noch bessere fachliche Ausdrucksform, als mit puren Lambda-Ausdrücken, erreicht werden.
Die wichtigste Erkenntnis ist aber wieder einmal: Es lohnt sich, einen Blick über den Java-Tellerrand zu wagen, um zu sehen, was in anderen Programmiersprachen möglich ist und ob man es vielleicht für sich selbst adaptieren kann.