Willkommen bei “Ruby is Magic – Behind the Scenes”. Wenn ihr euch noch an die letzte Episode erinnert, dann haben wir gezeigt, wie sich in Ruby Methoden als Closures verwenden lassen. Dazu haben wir das Decorator-Pattern ähnlich wie in Python implementiert.
Das Transkript der letzten Show war allerdings schon recht lang und daher sind wir nicht näher auf die Implementierung eingegangen. Da sie jedoch sehr interessant ist, wollen wir in diesem Artikel noch einmal im Detail darauf eingehen. Als kleinen Bonus haben wir das ganze auch einmal einem Benchmark unterzogen – natürlich völlig nicht-repräsentativ ;-)
Zunächst aber noch einmal kurz zum Hintergrund: Es ging vor
allem darum einen halbwegs realen Anwendungsfall für die Verwendung von
method()
zu finden. Das was am Ende dann hinten raus gefallen ist,
lässt sich unserer Meinung nach sogar tatsächlich in realen
Projekten einsetzen, denn das Decorator-Pattern ermöglicht durchaus elegante
Lösungen, wenn man eine Reihe unterschiedlicher Methoden mit
zusätzlichen Funktionalitäten anreichern möchte. In unserem Beispiel war
es dann eben das Hinzufügen einer Caching-Schicht.
Nehmen wir als Ausgangspunkt einmal ein stark vereinfachtes Objekt, um auf eine relationale Datenbank zuzugreifen:
1 2 3 4 5 |
|
Wir haben hier, wie so oft, unterschiedliche Möglichkeiten Caching
hinzuzufügen. Eine Variante wäre die Verwendung von alias_method
oder
eben alias_method_chain
, wenn ActiveSupport mit von der Partie ist. Das Ergebnis
ist, dass man eine weitere Methode in seiner Klasse definiert, die
das Caching implementiert. Dann stellen wir uns vor, dass wir noch zwei
bis drei weitere Methoden cachen wollen. Die einzelnen Implementierungen
werden dabei recht ähnlich zueinander sein und zusätzlich ist die Klasse
überladen mit Methoden, die nicht wirklich in ihren Aufgabenbereich
fallen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Das erste Problem lässt sich lösen, indem die Cache-Funktionalität in einer eigenen Klasse weiter gekapselt und diese in jeder Methode aufgerufen wird. Realisiert man diese Klasse nun noch als Decorator, haben wir auch das zweite Problem aus der Welt geschafft:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Die Konstruktion des Cache-Keys ist natürlich jetzt etwas komplexer geworden, aber irgendwas ist ja immer …
Implementierung
Kommen wir nun langsam zum interessanten Teil: Die Implementierung.
Vergleicht man die alias_method_chain
-Methode mit der
Decorator-Variante, dann fallen zwei Unterschiede auf:
- Die Methode
decorate
wird vor der zu dekorierenden Methode aufgerufen. - Der Name der zu dekorierenden Methode wird nicht angegeben – lediglich die Decorator-Klasse.
Überlegen wir uns zunächst einmal welche Schritte notwendig sein werden, um unsere Decorator Funktionalität umzusetzen:
- Erkennen welche Methode zu dekorieren ist
- Methode extrahieren –
method()
- Decorator mit extrahierter Methode inititalisieren
- Proxy Methode definieren
- Binding vor Ausführung der “alten” Methode umsetzen
Im Kontext von Closures sind nur die Punkte 2. und 5. von Relevanz. Der Rest ist im Grunde nur Glue-Code – Was ihn jedoch nicht weniger interessant macht.
Das ganze in Ruby-Code gegossen ergibt dann nicht einmal 40 Zeilen – wieder einmal ein schönes Beispiel dafür, wie ausdrucksstark diese Sprache ist:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
Die angesprochenen Unterschiede zu alias_method
haben wir durch einen
der zahlreichen (und teilweise nicht dokumentierten) Callbacks in Ruby
realisiert: Der Methodenaufruf decorate
merkt sich einfach die
Decorator-Klasse und sobald die nächste Methode definiert wird, wird
diese damit dekoriert. Auf das Hinzufügen einer Methode lässt sich dann mit
dem Callback method_added
warten.
Im Callback wird dann die zu dekorierende Methode extrahiert und durch
eine neue ersetzt. Die neue Methode braucht dabei nicht die gleiche
Signatur zu besitzen (das hatten wir in der letzten Episode noch
anders) – alle Parameter einsammeln und einen optionalen Block-Parameter
definieren reicht schon aus. Auf Klassenebene legen wir unter dem
Methodennamen noch die ursprüngliche Methode und eine Instanz der
Decorator-Klasse ab. Das ist notwendig, weil wir eine UnboundMethod
extrahieren, d.h. sie ist keinem Objekt zugeordnet und lässt sich damit
auch nicht aufrufen. Das bind
wird dann durchgeführt sobald die
neu-definierte Methode auf einer konkreten Instanz aufgerufen wird.
Damit erhalten wir eine Instanz von Method
, die sich wie ein Closure
aufrufen lässt.
Das ganze ist als Modul realisert, welches per extend
entweder direkt
in Object
oder etwas selektiver nur in die zu dekorierenden Klassen
eingebunden werden kann.
Zur Vollständigkeit hier noch die komplette Implementierung des
CacheDecorator
. Erwähnenswert ist noch, dass man über die receiver
auf der ursprünglichen Methode @component
die die Instanz des
dekorierten Objekts erhält. Somit hat man schon hier Zugriff auf den
Kontext von @component
und kann zum Beispiel den Cache-Key
konstruieren:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Benchmark
Wie versprochen gibt es am Ende noch einen nicht-repräsentativen
Benchmark für die Verwendung unserer Decorator-Implementierung. Wir
haben eine alternative Implementierung mit alias_method_chain
, also
unter Verwendung von ActiveSupport mit dem Decorator-Ansatz verglichen.
Konkret sah die Teststellung wie folgt aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
Wie erwartet ist die Decorator-Implementierung signifikant langsamer
als alias_method_chain
:
1 2 3 4 5 |
|
Aber es war ja auch nicht das Ziel alias_method_chain
zu ersetzen,
sondern einen Anwendungsfall für Methoden als Closures zu finden. Wir
denken, dass ist uns gut gelungen und wir werden diese Implementierung
definitiv irgendwo mal verwenden, denn es macht den Code durchaus
übersichtlicher und die Decorator besser testbar.
Das war es auch schon mit dem ersten “Behind the Scenes”. Wir hoffen es hat auch gefallen. Wir sehen uns bei der nächsten “Ruby is Magic”-Show am 15.02.2012 auf der cologne.rb.