Method chaining - also die implementierung einer Methode durch verketten mehrerer anderer ist durch Ruby on Rails zu einer gewissen Popularität gelangt. Während sich Rails hier auf ein einfaches Namenschema und alias verlässt, bietet DataMapper hier eine Technik an, die sich vorzüglich für einen Ausflug in Rubys Objektmodell eignet. Diese Gelegenheit will ich nicht an mir vorüber gehen lassen und die Implementierung namens Chainable hier vorstellen.
Wozu brauchen wir das Ganze eigentlich? Ruby ist eine Sprache, die besonders viel Wert auf die Komponentenweise konstruktion von Klassen legt. Anders als in Sprachen wie Java ist es durchaus erwünscht, Klassen in einzelnen Modulen zu implementieren und dann zusammen zu "mixen". Was aber, wenn bestimmte Methoden einer Klasse Verhalten gewinnen, wenn bestimmte Module inkludiert sind?
Nehmen wir als kleines Beispiel eine Klasse, die gespeichert werden kann:
1 2 3 4 5 |
class Person def save #speicher-fu! end end |
Hierzu schreiben wir uns ein Validierungsmodul. Validierung sollen immer ausgeführt werden, bevor die Person gespeichert wird:
1 2 3 4 5 6 7 8 9 10 |
module Validations def save validate! super end end class Person include Validations end |
Das sieht zwar schön aus, funktioniert aber leider nicht. Schauen wir uns die Ableitungskette von Person an, fällt auch sofort auf, wieso:
1 2 |
> Person.ancestors => [Person, Validations, Object, Kernel, BasicObject] |
Wird ein Modul per include in eine Klasse eingebunden, landet es
Um kurz den Rails-Weg anzureissen, hier deren Variante, dieses Problem zu lösen:
1 2 3 4 5 6 7 8 9 |
class Person def save_with_validations validate! save_without_validations end alias :save_without_validations :save alias :save :save_with_validations end |
Wir benennen also die alte Methode um, implementieren eine neue, die den neuen Namen der alten Methode verwendet und geben der die neue dann den alten Namen. Diese Lösung hat ihren Charme und durchaus Vorteile (ich kann z.B. immernoch sehr bequem save_with_validations aufrufen), aber auch Nachteile: ich muss diesen Code zum Beispiel immer beim Einmixen eines Moduls ausführen, wenn das Feature an einem Modul hängt. Auch ist die Kette schwer nachzuverfolgen.
Chainable geht einen anderen Weg. Die Implementierung ist kurz:
1 2 3 4 5 6 7 |
module Chainable def chainable(&block) mod = Module.new(&block) include mod mod end end # module Chainable |
Die Anwendung ist auch einfach:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Person extend Chainable chainable do def save #speicher-fu! end end end module Validations def save validate! super end end class Person include Validations end |
Ich markiere also eine oder mehrere Methode als verkettbar, was mir im Folgenden erlaubt, diese Methode als Anfang einer Methodenkette zu verwenden. Ein Blick auf Hierarchie zeigt, wo die Magie liegt:
1 2 |
> Person.ancestors => [Person, Validations, #<Module:0x000001009bb6e8>, Object, Kernel, BasicObject] |
Chainable erstellt also ein
Der Ansatz hat den Vorteil, dass alle Methoden einer Kette denselben Namen tragen und es ein klassisches Modell der Methodenverkettung verwendet: super. Ersteres kann natürlich von Nachteil sein, wenn man Methoden ohne Features nach Namen aufrufen will (z.B. save_without_validations). Allerdings stellt sich hier die Frage, ob Verkettung für solche Fälle die richtige Lösung ist: Welche Methode rufe ich auf, wenn ich auf 2 Features verzichten will?
Ein weiterer Vorteil ist die bessere Inspizierbarkeit: folgende kleine Methode gibt mir die volle Methodenkette für einen Namen aus, zusammen mit dem Ort, wo die Methoden definiert wurden (Achtung, source_location gibt es nur unter 1.9):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def filter_chain(klass, meth) chain = klass.ancestors.map do |a| begin m = a.instance_method(meth) next if m.owner != a [m, m.source_location] rescue NameError next end end chain.compact end filter_chain(Person, :save) |
Viel Spaß beim in Ketten legen, der volle Text über DataMapper kommt dann auch bald.
Kommentare
super erklärung! suche meinen weg momentan durch den source von rails und bin um solche allgemeinen erläuterungen von ruby-techniken daher sehr dankbar!!