Interpreter Design Pattern in Java

1. Übersicht

In diesem Tutorial stellen wir eines der verhaltensbezogenen GoF-Entwurfsmuster vor - den Interpreter.

Zunächst geben wir einen Überblick über den Zweck und erläutern das Problem, das es zu lösen versucht.

Anschließend werfen wir einen Blick auf das UML-Diagramm von Interpreter und die Implementierung des praktischen Beispiels.

2. Interpreter Design Pattern

Kurz gesagt, das Muster definiert die Grammatik einer bestimmten Sprache auf objektorientierte Weise, die vom Interpreter selbst bewertet werden kann.

Vor diesem Hintergrund könnten wir technisch gesehen unseren benutzerdefinierten regulären Ausdruck, einen benutzerdefinierten DSL-Interpreter oder eine der menschlichen Sprachen analysieren , abstrakte Syntaxbäume erstellen und dann die Interpretation ausführen.

Dies sind nur einige der möglichen Anwendungsfälle, aber wenn wir eine Weile darüber nachdenken, könnten wir noch mehr Verwendungen davon finden, zum Beispiel in unseren IDEs, da sie den Code, den wir schreiben, kontinuierlich interpretieren und uns damit versorgen unbezahlbare Hinweise.

Das Interpretermuster sollte im Allgemeinen verwendet werden, wenn die Grammatik relativ einfach ist.

Andernfalls kann die Wartung schwierig werden.

3. UML-Diagramm

Das obige Diagramm zeigt zwei Hauptentitäten: den Kontext und den Ausdruck .

Jetzt muss jede Sprache auf irgendeine Weise ausgedrückt werden, und die Wörter (Ausdrücke) werden basierend auf dem gegebenen Kontext eine Bedeutung haben.

AbstractExpression definiert eine abstrakte Methode, die den Kontext übernimmtals Parameter. Dank dessen wirkt sich jeder Ausdruck auf den Kontext aus , ändert seinen Status und setzt entweder die Interpretation fort oder gibt das Ergebnis selbst zurück.

Daher wird der Kontext der Inhaber des globalen Verarbeitungsstatus sein und während des gesamten Interpretationsprozesses wiederverwendet.

Was ist der Unterschied zwischen TerminalExpression und NonTerminalExpression ?

Einer NonTerminalExpression können eine oder mehrere andere AbstractExpressions zugeordnet sein, daher kann sie rekursiv interpretiert werden. Am Ende muss der Interpretationsprozess mit einer TerminalExpression abgeschlossen werden , die das Ergebnis zurückgibt.

Es ist erwähnenswert, dass NonTerminalExpression ein Verbund ist.

Schließlich besteht die Aufgabe des Clients darin, einen bereits erstellten abstrakten Syntaxbaum zu erstellen oder zu verwenden , der nichts anderes als ein in der erstellten Sprache definierter Satz ist.

4. Implementierung

Um das Muster in Aktion zu zeigen, erstellen wir eine einfache SQL-ähnliche Syntax auf objektorientierte Weise, die dann interpretiert wird und uns das Ergebnis zurückgibt.

Zuerst definieren wir die Ausdrücke Select, From und Where , erstellen einen Syntaxbaum in der Klasse des Clients und führen die Interpretation aus.

Die Ausdrucksschnittstelle verfügt über die Interpretationsmethode:

List interpret(Context ctx);

Als nächstes definieren wir den ersten Ausdruck, die Select- Klasse:

class Select implements Expression { private String column; private From from; // constructor @Override public List interpret(Context ctx) { ctx.setColumn(column); return from.interpret(ctx); } }

Es erhält den auszuwählenden Spaltennamen und einen weiteren konkreten Ausdruck vom Typ From als Parameter im Konstruktor.

Beachten Sie, dass bei der überschriebenen interpret () -Methode der Status des Kontexts festgelegt und die Interpretation zusammen mit dem Kontext an einen anderen Ausdruck übergeben wird.

Auf diese Weise sehen wir, dass es sich um einen NonTerminalExpression handelt.

Ein weiterer Ausdruck ist die From- Klasse:

class From implements Expression { private String table; private Where where; // constructors @Override public List interpret(Context ctx) { ctx.setTable(table); if (where == null) { return ctx.search(); } return where.interpret(ctx); } }

In SQL ist die where-Klausel optional, daher ist diese Klasse entweder ein Terminal- oder ein Nicht-Terminal-Ausdruck.

Wenn der Benutzer beschließt, keine where-Klausel zu verwenden, wird der From- Ausdruck mit dem Aufruf von ctx.search () beendet und das Ergebnis zurückgegeben. Andernfalls wird es weiter interpretiert.

Der Where- Ausdruck ändert den Kontext erneut, indem der erforderliche Filter festgelegt wird, und beendet die Interpretation mit einem Suchaufruf:

class Where implements Expression { private Predicate filter; // constructor @Override public List interpret(Context ctx) { ctx.setFilter(filter); return ctx.search(); } }

In diesem Beispiel enthält die Context- Klasse die Daten, die die Datenbanktabelle imitieren.

Beachten Sie, dass es drei Schlüsselfelder gibt, die von jeder Unterklasse von Ausdruck und der Suchmethode geändert werden :

class Context { private static Map
    
      tables = new HashMap(); static { List list = new ArrayList(); list.add(new Row("John", "Doe")); list.add(new Row("Jan", "Kowalski")); list.add(new Row("Dominic", "Doom")); tables.put("people", list); } private String table; private String column; private Predicate whereFilter; // ... List search() { List result = tables.entrySet() .stream() .filter(entry -> entry.getKey().equalsIgnoreCase(table)) .flatMap(entry -> Stream.of(entry.getValue())) .flatMap(Collection::stream) .map(Row::toString) .flatMap(columnMapper) .filter(whereFilter) .collect(Collectors.toList()); clear(); return result; } }
    

Nachdem die Suche abgeschlossen ist, wird der Kontext selbst gelöscht, sodass Spalte, Tabelle und Filter auf die Standardeinstellungen gesetzt werden.

Auf diese Weise wirkt sich jede Interpretation nicht auf die andere aus.

5. Testen

Schauen wir uns zu Testzwecken die InterpreterDemo- Klasse an:

public class InterpreterDemo { public static void main(String[] args) { Expression query = new Select("name", new From("people")); Context ctx = new Context(); List result = query.interpret(ctx); System.out.println(result); Expression query2 = new Select("*", new From("people")); List result2 = query2.interpret(ctx); System.out.println(result2); Expression query3 = new Select("name", new From("people", new Where(name -> name.toLowerCase().startsWith("d")))); List result3 = query3.interpret(ctx); System.out.println(result3); } }

Zuerst erstellen wir einen Syntaxbaum mit erstellten Ausdrücken, initialisieren den Kontext und führen dann die Interpretation aus. Der Kontext wird wiederverwendet, aber wie oben gezeigt, bereinigt er sich nach jedem Suchaufruf von selbst.

Wenn Sie das Programm ausführen, sollte die Ausgabe wie folgt aussehen:

[John, Jan, Dominic] [John Doe, Jan Kowalski, Dominic Doom] [Dominic]

6. Nachteile

Wenn die Grammatik komplexer wird, wird es schwieriger, sie beizubehalten.

Dies ist im vorliegenden Beispiel zu sehen. Es wäre ziemlich einfach, einen anderen Ausdruck wie Limit hinzuzufügen , aber es wäre nicht allzu einfach, ihn beizubehalten, wenn wir ihn mit allen anderen Ausdrücken erweitern würden.

7. Fazit

Das Interpreter-Entwurfsmuster eignet sich hervorragend für eine relativ einfache Grammatikinterpretation , die sich nicht weiterentwickeln und erweitern muss.

Im obigen Beispiel haben wir gezeigt, dass es möglich ist, mit Hilfe des Interpreter-Musters eine SQL-ähnliche Abfrage objektorientiert zu erstellen.

Schließlich finden Sie diese Musterverwendung in JDK, insbesondere in java.util.Pattern , java.text.Format oder java.text.Normalizer .

Wie üblich ist der vollständige Code im Github-Projekt verfügbar.