Lazy Values mit FreshMarker

“Progress isn’t made by early risers. It’s made by lazy men trying to find easier ways to do something.”

Robert A. Heinlein

Bei der Arbeit mit Legacy Anwendungen ist es einem Entwickler selten vergönnt, Ideen für eine neue Software zu finden. Hin und wieder zeigt sich aber ein Problem, das auch in anderen Konstellationen auftreten kann.

Bei der Arbeit mit der Template Engine FreshMarker müssen bisher alle notwendigen Daten für die Verarbeitung in einer Map an das Template übergeben werden. Müssen diese Daten aufwendig bereitgestellt werden und werden dann nicht genutzt, ist dies eine Verschwendung teurer Resourcen.

Person person = fetchPersonByName("Christoph /Kayser/");
Person father = fetchPerson(person.getFather());
Person mother = fetchPerson(person.getMother());

Configuration configuration = new Configuration();
    Template template = configuration.getTemplate("test", """
        <#if (person.father)?? && (person.mother)??>
        Vater:  ${father.fullName}
        Mutter: ${mother.fullName}
        </#if>
        """);
String result = template.process(Map.of("person", person, "father", father, "mother", mother));

Im obigen Beispiel werden die Eltern meines Ahnen Christoph Kayser aus der Datenbank gelesen und als Map-Elemente father und mother an das Template übergeben. Der generierte Text ist der folgende Zweizeiler.

Vater:  Diedrich Kayser
Mutter: Anna Margret Tribben

Ob die Eltern ausgegeben werden, entscheidet sich in diesem konstruierten Beispiel innerhalb des Templates. Die Ausgabe soll eben nur dann erzeugt werden, wenn beide Eltern in der Datenbank existieren. Ärgerlicherweise müssen aber beide Personen in der Datenbank abgefragt werden, weil der Java Code nichts über die If-Anweisung im Template weiß.

Eine Lösung für dieses Problem ist der Einbau von Lazy Evaluation für die Eltern Instanzen. Erst wenn tatsächlich auf diese Werte zugegriffen wird, werden sie auch tatsächlich berechnet.

Zentraler Verarbeitungspunkt der Template-Variablen ist die wrap Methode im BaseEnvironment. Hier werden die eigentlichen Werte in TemplateObject Wrapper eingefügt.

private TemplateObject wrap(Object o) {
  if (o == null) {
    return TemplateNull.NULL;
  }
  if (o instanceof TemplateObject templateObject) {
    return templateObject;
  }
  Object current;
  if (o instanceof TemplateObjectSupplier templateObject) {
    current = templateObject.get();
  } else {
    current = o;
  }
  return providers.stream().map(p -> p.provide(this, current)).filter(Objects::nonNull)
      .findFirst().orElseThrow(() -> new UnsupportedDataTypeException("unsupported data type: " + o.getClass()));
}

Zu den beiden bisherigen Ausnahmen für null und TemplateObject Instanzen, gesellt sich nun die Behandlung von TemplateObjectSupplier Instanzen hinzu. Hier wird die TemplateObjectSupplier Instanz durch das Ergebnis ihrer get Methode ersetzt.

public interface TemplateObjectSupplier<T> extends Supplier<T> {

  static <S> TemplateObjectSupplier<S> of(Supplier<S> supplier) {
    return supplier::get;
  }
}

Das Interface TemplateObjectSupplier erweitert das Supplier Interface und liefert eine statische Methode of zum Erzeugen von Instanzen. Das Supplier Interface wird nicht direkt verwendet, falls irgendjemand Supplier Instanzen als Templatevariablen verwenden möchte.

Das verbesserte Beispiel nutzt nun für die father und mother Map-Elemente jeweils einen TemplateObjectSupplier, der bei Bedarf die Person aus der Datenbank liest.

Person person = fetchPersonByName("Christoph /Kayser/");

Configuration configuration = new Configuration();
    Template template = configuration.getTemplate("test", """
        <#if (person.father)?? && (person.mother)??>
        Vater: ${father.fullName}
        Mutter: ${mother.fullName}
        </#if>
        """);
String result = template.process(
     Map.of("person", person, 
            "father", TemplateObjectSupplier.of(() -> fetchPerson(person.getFather()), 
            "mother", TemplateObjectSupplier.of(() -> fetchPerson(person.getMother())));

Damit ist die Implementierung von Lazy Values in FreshMarker auch schon wieder beendet und ein ähnliche Lösung kann nun auch in der Legacy Anwendung implementiert werden.

Schreibe einen Kommentar