Javaのインタプリタデザインパターン

Javaでの通訳者設計パターン

1. 概要

このチュートリアルでは、行動的なGoFデザインパターンの1つであるインタープリターを紹介します。

最初に、その目的の概要を示し、解決しようとする問題について説明します。

次に、インタープリターのUML図と実際の例の実装を見ていきます。

2. 通訳者デザインパターン

要するに、パターンdefines the grammar of a particular languageは、インタプリタ自体が評価できるオブジェクト指向の方法です。

そのことを念頭に置いて、技術的には、カスタム正規表現、カスタムDSLインタープリターを構築するか、任意の人間の言語build abstract syntax trees and then run the interpretation.を解析することができます。

これらは潜在的なユースケースのほんの一部ですが、しばらく考えてみると、たとえばIDEで、作成中のコードを継続的に解釈して提供しているため、さらに多くの使用法を見つけることができます。貴重なヒント。

通常、インタープリターパターンは、文法が比較的単純な場合に使用する必要があります。

そうしないと、保守が難しくなる可能性があります。

3. UML図

image

上の図は、ContextExpressionの2つの主要なエンティティを示しています。

さて、言語は何らかの方法で表現する必要があり、単語(表現)は与えられたコンテキストに基づいて何らかの意味を持つようになります。

AbstractExpression は、コンテキスト をパラメーターとして受け取る1つの抽象メソッドを定義します。 そのおかげで、each expression will affect the contextはその状態を変更し、解釈を続行するか、結果自体を返します。

したがって、コンテキストはグローバルな処理状態の保持者になり、解釈プロセス全体で再利用されます。

では、TerminalExpressionNonTerminalExpressionの違いは何ですか?

NonTerminalExpressionには、他の1つ以上のAbstractExpressionsが関連付けられている可能性があるため、再帰的に解釈できます。 結局、the process of interpretation has to finish with a TerminalExpression that will return the result.

NonTerminalExpressioncomposite.であることに注意してください

最後に、クライアントの役割は、作成済みのabstract syntax treeを作成または使用することです。これは、sentence defined in the created language.にすぎません。

4. 実装

パターンの動作を示すために、オブジェクト指向の方法で単純なSQLのような構文を作成します。これが解釈され、結果が返されます。

まず、Select, From,式とWhere式を定義し、クライアントのクラスで構文ツリーを構築して、解釈を実行します。

Expressionインターフェースには次の解釈メソッドがあります。

List interpret(Context ctx);

次に、最初の式であるSelectクラスを定義します。

class Select implements Expression {

    private String column;
    private From from;

    // constructor

    @Override
    public List interpret(Context ctx) {
        ctx.setColumn(column);
        return from.interpret(ctx);
    }
}

選択する列名と、コンストラクターのパラメーターとしてタイプFromの別の具象Expressionを取得します。

オーバーライドされたinterpret()メソッドでは、コンテキストの状態を設定し、解釈をコンテキストとともに別の式にさらに渡すことに注意してください。

そうすれば、NonTerminalExpression.であることがわかります

別の式はFromクラスです。

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);
    }
}

現在、SQLではwhere句はオプションであるため、このクラスは終端式または非終端式です。

ユーザーがwhere句を使用しないことを決定した場合、From式は、ctx.search()呼び出しで終了し、結果を返します。 それ以外の場合は、さらに解釈されます。

Where expressionは、必要なフィルターを設定することによってコンテキストを再度変更し、検索呼び出しで解釈を終了します。

class Where implements Expression {

    private Predicate filter;

    // constructor

    @Override
    public List interpret(Context ctx) {
        ctx.setFilter(filter);
        return ctx.search();
    }
}

たとえば、Context classは、データベーステーブルを模倣しているデータを保持します。

Expressionの各サブクラスと検索メソッドによって変更される3つのキーフィールドがあることに注意してください。

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;
    }
}

検索が完了すると、コンテキストは自動的にクリアされるため、列、テーブル、およびフィルターはデフォルトに設定されます。

そうすれば、それぞれの解釈が他の解釈に影響を与えることはありません。

5. テスト

テストの目的で、InterpreterDemo classを見てみましょう。

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);
    }
}

まず、作成された式で構文ツリーを構築し、コンテキストを初期化してから解釈を実行します。 コンテキストは再利用されますが、上で示したように、各検索呼び出しの後に自動的に削除されます。

プログラムを実行すると、出力は次のようになります。

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

6. 欠点

文法が複雑になると、保守が難しくなります。

提示された例でそれを見ることができます。 Limitのような別の式を追加するのはかなり簡単ですが、他のすべての式で拡張し続けることにした場合、維持するのはそれほど簡単ではありません。

7. 結論

インタープリターのデザインパターンは素晴らしいfor relatively simple grammar interpretationであり、それほど進化したり拡張したりする必要はありません。

上記の例では、インタプリタパターンの助けを借りて、オブジェクト指向の方法でSQLのようなクエリを作成できることを示しました。

最後に、このパターンの使用法はJDK、特にjava.util.Patternjava.text.Format、またはjava.text.Normalizerで確認できます。

いつものように、完全なコードはthe Github projectで入手できます。