Syntaktischer Zucker: Ruby-Methoden mit Annotationen versehen

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.