Ruby enthält ein nettes Feature, das es ermöglicht, Methoden bei ihrer Definition leicht mit Annotationen zu versehen. Wir haben das ausgenutzt, um “schnelle” Methodenvarianten zur Verfügung zu stellen, die ihren Rückgabewert cachen.
Seit Ruby 2.1 liefern Methodendefinitionen den Namen der Methode als Symbol zurück.
def foo; end # ⇒ :foo
define_method(:foo) do; end # ⇒ :foo
Dieser Rückgabewert wiederum lässt sich toll als Parameter eines weiteren Methodenaufrufs verwenden, zum Beispiel:
protected def foo; end
before_action def load_book
@book = Book.find(params[:id]
end
Damit lässt sich das Konzept der Java-Annotationen sehr elegant auch in Ruby einsetzen.
Nun zu unserem Anwendungsfall: Einige unserer Domänen-Objekte, die primär Content bereitstellen, wurden unnötigerweise mehrfach innerhalb eines Requests einer Rails-Anwendung aus der Datenbank geladen. Unser Ansatz war, den entsprechenden Lookup-Methoden eine Variante danebenzustellen, die die geladenen Objekte automatisch cachet.
class City
class << self
extend FastFind
[...]
private
fast def find(id)
# Langsamer DB-Lookup etc.
sleep 1
new(name: 'Dummy')
end
end
[...]
end
In allen Programmteilen, die also Ortsdaten lediglich lesend benötigen,
konnte City.find(id)
durch City.fast_find(id)
ersetzt werden.
Die Implementierung des Moduls ist ebenfalls recht kompakt:
module FastFind
def fast(method)
# Neue Methode mit "fast_"-Präfix definieren
define_method "fast_#{method}" do |query|
# Cache-Key ermitteln
lookup_key = "#{name}_#{method}_#{query}"
if RequestStore.exist?(lookup_key)
RequestStore[lookup_key]
else
# Eigentliche Methode aufrufen
# und Ergebnis gefreezt im Store ablegen
send(method, query).tap do |obj|
RequestStore[lookup_key] = obj.freeze
end
end
end
end
RequestStore stellt die Funktionalität bereit, Thread-lokal, Request-Daten abzulegen und sich eben auch um die Aufräumarbeiten zu kümmern. Wir verwenden das gefreezete Objekt, um Änderungen daran zu verhindern. Möchten wirklich einzelne Programmteile das Objekt schreibend beziehungsweise speicherbar verwenden, dann können diese nicht die “schnelle” Methode verwenden, sondern müssen immer das aktuellste Objekt auschecken.
Bevor die Frage aufkommt, warum wir nicht
memoist oder das Ruby-Sprachkonstrukt
||=
einsetzen, hier gleich die Antwort: Diese Lösungen würden in unserem
Fall der zu cachenden Klassenmethoden die Ergebnisse ewig speichern.
Das ist aber nicht in unserem Sinne.