Alles Dynamisch


 ... [ Groovy - Titelseite ] ... [ << Funktionale Elemente ] ... [ Fazit >> ] ...  




Übersicht:

  Methoden-Dispatch
  Properties - Attribute
  Dynamisches Hinzufügen von Methoden
  Evaluieren: Daten als Code



Methoden-Dispatch

Bei einem Methodenaufruf gibt es in Java die Möglichkeit Methoden zu überladen, das heißt mehrere Methoden zu definieren, die den selben Namen, aber unterschiedliche Parameterlisten besitzen. Es wird dabei immer zur Kompilierungszeit entschieden, welche der Methoden tatsächlich aufgerufen wird. In Groovy ist das Überladen ebenfalls möglich. Es wird hier aber erst dynamisch (zur Laufzeit) entschieden, also anhand der Laufzeit-Typen der Parameter, welche Methode aufgerufen wird. Der folgende Vergleich zwischen einer Java-Datei und der entsprechenden Groovy-Datei soll dies verdeutlichen.

Java:

class Dispatch
{
    static int foo(Object o)
    {
        return 1;
    }
    static int foo(String s)
    {
        return 2;
    }
    public static void main(String[] args)
    {
        Object o = new Object();
        Object s = "schall";
        assert( foo(o) == 1);
        assert( foo(s) == 1); //!
    }
}
Groovy:
class Dispatch
{
    static int foo(Object o)
    {
        return 1;
    }
    static int foo(String s)
    {
        return 2;
    }
    public static void main(String[] args)
    {
        Object o = new Object();
        Object s = "rauch";
        assert foo(o) == 1;
        assert foo(s) == 2; //!
    }
}
Die Methoden müssen nicht zwingend mit static attribuiert werden, dies vereinfacht nur das Beispiel. Nicht-statische Methoden verhalten sich analog.

Das folgende Beispiel zeigt, dass eine DoubleDispatch nur dann möglich ist, wenn eindeutig entschieden werden kann, welche Methode aufgerufen werden soll. Eine Fehlermeldung ist hier natürlich nur zur Laufzeit möglich.

class Dispatch
{
    static int foo(Object o, String s)
    {
        return 1;
    }
    static int foo(String s, Object o)
    {
        return 2;
    }
    public static void main(String[] args)
    {
        Object o = new Object();
        Object s = "rauch";
        try {
            assert foo(o, o) == 1;
            assert false;
        } catch(MissingMethodException e) {
            assert true;
            println "(o,o): ${e.message}\n";
        }

        try {
            assert foo(s, s) == 2;
            assert false;
        } catch(GroovyRuntimeException e) {
            assert true;
            println "(s,s): ${e.message}\n";
        }
    }
}
Bei dem Aufruf mit zwei Objects deutet die Fehlermeldung darauf, dass es gar keine Methode foo gibt, die aufgerufen werden soll. Die Meldung beim Aufruf mit zwei Strings weist dagegen eindeutig darauf hin, dass nicht entschieden werden kann, welche Methode aufgerufen werden soll. Zusätzlich erhält man eine Liste von Methodensignaturen, die alle zu dem Aufruf passen.

In Java sind Methodenaufrufe normalerweise etwas Statisches. Es gibt aber auch hier die Möglichkeit, mit Hilfe der Methode Object invokeMethod(String name, Object[] args) Methoden dynamisch aufzurufen. In Groovy werden Methoden implizit immer mittels invokeMethod aufgerufen. Daher darf der . Operator auch einen String anstatt einem Methodenbezeichner annehmen:

class A {
    public int foo () {
        return 42;
    }   
}
String s = "foo";
A a = new A();
assert 42 == a."foo"();
assert 42 == a.foo();
assert 42 == a."${s}"(); // not yet implemented
Vorsicht: Dieses Feature wurde noch nicht vollständig implementiert. Ein einfaches Beispiel, welches schon lauffähig ist:
public int foo () {
    return 42;
}
String s = "foo";
assert 42 == "foo"();
assert 42 == "$s"();

Der . Operator wurde in Groovy mit der Methode invokeMethod überladen, so dass es möglich ist Methodenaufrufe für Objekte bestimmter Klassen abzufangen. Dieses Verfahren kann zum Beispiel fürs Logging nützlich sein. Ein anderer Anwendungsfall ist das Erzeugen Baum-artiger Datenstrukturen, wie zum Beispiel XML oder HTML. Ein einfaches, leeres HTML-Dokument, könnte man zum Beispiel wie folgt erzeugen:

HtmlCreator c = new ...;
String content = c.html {
    head {
        "der kopf";
    }
    body {
        "der body-inhalt";
    }
}
println content

Die Ausgabe des Skripts:

          <html>
          <head>
          "der kopf"
          </head>
          <body>
          "der body-inhalt"
          </body>
          </html>
        

html ist dabei keine Methode aus HtmlCreator und verursacht somit den Aufruf von invokeMethod, welches ein HTML-Tag erzeugt, da der Name der Methode "html" ist. Entsprechendes gilt für die anderen Methoden. Der interessante Punkt hierbei ist die Möglichkeit, beliebigen Code innerhalb der verschachtelten Closures einzubetten. Man muß also nicht mehr verschiedene Sprachen miteinander mischen (z.B. Javascript und HTML), sondern kann alles in einer Sprache erledigen. Es besteht auch die Möglichkeit, eigene "Builder" zu implementieren, um selbst definierte Baum-artige Strukturen einfacher eingeben zu können.



Properties - Attribute

Wenn es möglich ist, den . Operator für Methoden zu überladen, dann sollte das gleiche auch mit Attributen möglich sein. Dazu dienen die Methoden getProperty und setProperty, die jede Klasse beliebig überschreiben kann. Die Map- und Closure -Klassen machen davon Gebrauch.

class Property
{
    public Object getProperty(String name)
    {
        return "Schall";
    }
    public void setProperty(String name, Object value)
    {
        println "$name: $value";
    }
}
Property p = new Property();
assert p.a == "Schall";
assert p.egalWasHierSteht == "Schall";
p.foo = "Schall";
p.bar = "Rauch";
Das Beispiel erzeugt die Ausgabe:
        foo: Schall
        bar: Rauch
      


Dynamisches Hinzufügen von Methoden

Manchmal benötigt man als Programmierer eine Methoden in einer Klasse, auf die man keinen Zugriff auf Quellcode-Ebene hat. Wir betrachten hier beispielhaft die Methode dup() die einen String verdoppeln soll. In Java müsste man eine Hilfsklasse schreiben, die eine Klassen-Methode enthält:

class StringFunctions
{
    public static String dup(String self)
    {
        return s + s;
    }
}
Dadurch muß man allerdings bei jedem Aufruf von dup folgendes schreiben:
StringsFunctions.dup("foo")
Es ist nicht möglich, die in der OOP gebräuchliche Syntax zu nutzen:
"foo".dup()
In Groovy ist es durch so genannte "categories" möglich, einer Klasse zur Laufzeit Methoden hinzuzufügen, und somit auch die gewünschte Syntax zu nutzen:
class StringFunctions {
    public static def dup(String a) {
        return a + a;
    }
}
use (StringFunctions.class) {
    assert "11" == '1'.dup();
    assert "4242" == '42'.dup();
}
Durch das Überschreiben von bestimmten (oben genannten) Methoden wird es auch möglich, Operatoren beliebiger Klassen zu überladen. Da man durch categories auch sicherheitsrelevante Methoden überschreiben kann, sind categories nur im aktuellen Thread, und nur in der an die Methode use übergebene Closure nutzbar. Categories eignen sich somit zum Loggen eines bestimmten Quelltextabschnittes.



Evaluieren: Daten als Code

Eine der typischen Operationen in einer Skriptsprache ist das Interpretieren eines Strings. Eine einfache Möglichkeit zur Auswertung ist die Methode Object.evaluate. Die Methode ist mehrfach überladen; als Parameter kann zum Beispiel ein String, eine Datei oder ein Eingabestrom übergeben werden.

String s = "7**2 - 7;";
assert 42 == evaluate(s);
Der Rückgabewert der eval-Methode richtet sich nach der letzten ausgeführten Anweisung. Das return Schlüsselwort ist auch hier optional.

Möchte man dem Skript auch Parameter übergeben, benötigt man dafür ein Binding Objekt. Ein Binding-Objekt ist eine Art Scope (Sichtbarkeitsbereich), oder anders ausgedrückt, eine Tabelle mit Variablennamen und Variablenwerten. Dem Binding Objekt kann man eine Map übergeben, in der man Variablen vordefiniert. Das Binding übergibt man dann einer GrooyShell, welche wiederum eine evaluate Methode hat.

def binding = new Binding(x: 6, y: 4);
def shell = new GroovyShell(binding);
shell.evaluate('''
xSquare = x * x;
yCube = y * y * y;
''');
assert binding.getVariable("xSquare") == 36;
assert binding.yCube == 64;

Das Beispiel zeigt, dass auf die Variablen des Binding auch mit der einfachen Syntax binding.VARIABLEN_NAME zugegriffen werden kann. Zusätzlich gibt es die Methoden Object getVariable(String) und void setVariable(String, Object), um auf Variablen zuzugreifen. Im Gegensatz zu Tcl ist die Methode mit dem zwischengeschalteten Binding zwar etwas mehr Tipparbeit, der Vorteil ist aber die erhöhte Sicherheit: Es können keine Variablen des gleichen Gültigkeitsbereichs überschrieben werden.




 ... [ Groovy - Titelseite ] ... [ << Funktionale Elemente ] ... [ Fazit >> ] ... [ nach oben ] ...