“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.