Interpreter-Entwurfsmuster in Java

Interpreter Design Pattern in Java

1. Überblick

In diesem Tutorial stellen wir eines der Verhaltensmuster des GoF-Designs vor - den Interpreter.

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

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

2. Interpreter-Entwurfsmuster

Kurz gesagt, das Musterdefines the grammar of a particular languageist objektorientiert und kann vom Interpreter selbst ausgewertet werden.

In Anbetracht dessen könnten wir technisch gesehen unseren benutzerdefinierten regulären Ausdruck, einen benutzerdefinierten DSL-Interpreter, erstellen oder eine der menschlichen Sprachenbuild abstract syntax trees and then run the interpretation. analysieren

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 Interpretenmuster sollte im Allgemeinen verwendet werden, wenn die Grammatik relativ einfach ist.

Andernfalls kann die Wartung schwierig werden.

3. UML-Diagramm

image

Das obige Diagramm zeigt zwei Hauptentitäten:Context undExpression.

Nun muss jede Sprache in irgendeiner Weise ausgedrückt werden, und die Wörter (Ausdrücke) werden eine Bedeutung haben, die auf dem gegebenen Kontext basiert.

AbstractExpression definiert eine abstrakte Methode, die den Kontext als Parameter verwendet. Dank dessen änderteach expression will affect the context seinen Zustand 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 zwischenTerminalExpression undNonTerminalExpression?

EinemNonTerminalExpression können ein oder mehrere andereAbstractExpressions zugeordnet sein, daher kann es rekursiv interpretiert werden. Am Endethe process of interpretation has to finish with a TerminalExpression that will return the result.

Es ist anzumerken, dassNonTerminalExpression eincomposite. ist

Schließlich besteht die Aufgabe des Clients darin, bereits erstellteabstract syntax tree zu erstellen oder zu verwenden, die nicht mehr alssentence defined in the created language. sind

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ückeSelect, From, undWhere, erstellen einen Syntaxbaum in der Klasse des Clients und führen die Interpretation aus.

DieExpression-Schnittstelle verfügt über die Interpretationsmethode:

List interpret(Context ctx);

Als nächstes definieren wir den ersten Ausdruck, die KlasseSelect:

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 weitere konkreteExpressionvom TypFromals Parameter im Konstruktor.

Beachten Sie, dass in der überschriebeneninterpret()-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 einNonTerminalExpression.ist

Ein weiterer Ausdruck ist die KlasseFrom:

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 terminaler oder ein nicht terminaler Ausdruck.

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

DieWhere -Sexpression ändert erneut den Kontext, 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 dieContext -Skala die Daten, die die Datenbanktabelle imitieren.

Beachten Sie, dass es drei Schlüsselfelder gibt, die von jeder Unterklasse vonExpression 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, löscht sich der Kontext von selbst, sodass Spalte, Tabelle und Filter auf Standardwerte gesetzt werden.

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

5. Testen

Schauen wir uns zu Testzwecken dieInterpreterDemo -Skala 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.

Wenn Sie das Programm ausführen, sollte die Ausgabe folgendermaßen 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 vorgestellten Beispiel zu sehen. Es wäre ziemlich einfach, einen weiteren Ausdruck wieLimit hinzuzufügen, aber es wäre nicht allzu einfach, ihn zu pflegen, wenn wir ihn weiterhin mit allen anderen Ausdrücken erweitern würden.

7. Fazit

Das Interpreter-Entwurfsmuster ist großartigfor relatively simple grammar interpretation, was nicht viel weiterentwickelt und erweitert werden muss.

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

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

Wie üblich ist der vollständige Code fürthe Github project verfügbar.