java-recipes

ホーム デザインパターン › DP-15

DP-15: Interpreter パターン

特定の言語や文法をクラス群で表現し、文を解釈・評価するパターンです。 「式」をクラスで表し、それらを木構造(式ツリー)に組み合わせて複雑な表現を解釈します。

Interpreter パターンとは

Interpreter(インタープリター)パターンは、特定の言語や文法に従った「文(sentence)」を 解釈・評価するパターンです。文法の各規則をクラスとして表現し、それらを組み合わせて 複雑な式を構築します。

登場する役割

  • AbstractExpression(抽象式):interpret() メソッドを定義するインターフェース
  • TerminalExpression(終端式): それ以上分解できない最小単位の式(例: 数値リテラル)
  • NonterminalExpression(非終端式): 他の式を組み合わせた複合式(例: 加算、乗算)
  • Context: インタープリターが参照するグローバルな情報(変数テーブルなど)

主なユースケースとして、設定ファイルの式評価(例: フィルター条件の文字列を解析する)、 SQL の WHERE 句のような条件式の評価、テンプレートエンジンの変数展開などがあります。 標準ライブラリでは java.util.regex.Pattern(正規表現)が Interpreter パターンに基づいた設計の代表例です。

下のサンプルでは、四則演算(加算・減算・乗算・除算)の簡易インタープリターを実装します。(3 + 5) * 2 - 4 のような式をクラスの木構造として表現し、interpret() を再帰的に呼び出して評価します。

サンプルコード

InterpreterPatternSample.java
import java.util.ArrayList;
import java.util.List;

public class InterpreterPatternSample {

    // 抽象式(Abstract Expression): 式を評価するインターフェース
    interface Expression {
        int interpret();
    }

    // 終端式(Terminal Expression): 数値リテラルを表す
    static class NumberExpression implements Expression {
        private final int value;

        NumberExpression(int value) {
            this.value = value;
        }

        @Override
        public int interpret() {
            // 数値そのものを返す
            return value;
        }
    }

    // 非終端式(Non-terminal Expression): 加算を表す
    static class AddExpression implements Expression {
        private final Expression left;
        private final Expression right;

        AddExpression(Expression left, Expression right) {
            this.left = left;
            this.right = right;
        }

        @Override
        public int interpret() {
            // 左辺と右辺の評価結果を足し合わせる
            return left.interpret() + right.interpret();
        }
    }

    // 非終端式: 減算を表す
    static class SubtractExpression implements Expression {
        private final Expression left;
        private final Expression right;

        SubtractExpression(Expression left, Expression right) {
            this.left = left;
            this.right = right;
        }

        @Override
        public int interpret() {
            return left.interpret() - right.interpret();
        }
    }

    // 非終端式: 乗算を表す
    static class MultiplyExpression implements Expression {
        private final Expression left;
        private final Expression right;

        MultiplyExpression(Expression left, Expression right) {
            this.left = left;
            this.right = right;
        }

        @Override
        public int interpret() {
            return left.interpret() * right.interpret();
        }
    }

    // 非終端式: 除算を表す
    static class DivideExpression implements Expression {
        private final Expression left;
        private final Expression right;

        DivideExpression(Expression left, Expression right) {
            this.left = left;
            this.right = right;
        }

        @Override
        public int interpret() {
            int divisor = right.interpret();
            if (divisor == 0) {
                throw new ArithmeticException("ゼロ除算は許可されていません");
            }
            return left.interpret() / divisor;
        }
    }

    public static void main(String[] args) {
        // (3 + 5) * 2 - 4 を式ツリーで表現する
        Expression expr = new SubtractExpression(
            new MultiplyExpression(
                new AddExpression(
                    new NumberExpression(3),
                    new NumberExpression(5)
                ),
                new NumberExpression(2)
            ),
            new NumberExpression(4)
        );

        System.out.println("(3 + 5) * 2 - 4 = " + expr.interpret());
        // 結果: 12

        // シンプルな例: 10 + 20
        Expression simple = new AddExpression(
            new NumberExpression(10),
            new NumberExpression(20)
        );
        System.out.println("10 + 20 = " + simple.interpret());
        // 結果: 30

        // 除算の例: (100 - 40) / 6
        Expression divExpr = new DivideExpression(
            new SubtractExpression(
                new NumberExpression(100),
                new NumberExpression(40)
            ),
            new NumberExpression(6)
        );
        System.out.println("(100 - 40) / 6 = " + divExpr.interpret());
        // 結果: 10
    }
}

Java 8 では、抽象式を interface で定義し、各種の演算を内部クラス(static class)として実装します。式ツリーを再帰的に組み合わせることで複雑な式を表現できます。

よくあるミス・注意点

⚠️ 文法が複雑な場合は Interpreter パターンを使わない

Interpreter パターンは文法のルールが少ない場合(10〜20程度)に適しています。 SQL や JavaScript のような複雑な文法に適用すると、クラス数が爆発的に増えてメンテナンスが困難になります。 複雑な文法が必要な場合は ANTLR などの専用パーサーライブラリを使用しましょう。

⚠️ ゼロ除算のチェックを忘れる

除算を含む式を評価する際、右辺の評価結果が 0 になる可能性があります。interpret() を呼んだ時点で右辺を評価してゼロチェックを行いましょう。 事前に静的にチェックすることはできないため、実行時のチェックが必須です。

⚠️ 式ツリーが深すぎるとスタックオーバーフローが発生する

interpret() は再帰呼び出しで実装されます。 式が何千段階もネストするような場合は Java のスタックが枯渇してStackOverflowError が発生します。 実用的な範囲では問題になりませんが、ユーザー入力を評価する場合はネストの深さに上限を設けましょう。

⚠️ Java 21 の switch で sealed でない interface を使うとコンパイルエラー

Java 21 の switch パターンマッチングで網羅性チェックを有効にするには、 対象の型が sealed である必要があります。 通常の interface では未知のサブタイプが存在しうるため、default ケースが必要になります。

テストする観点

  • NumberExpression が指定した値をそのまま返すこと(終端式の正常系)
  • 加算・減算・乗算・除算のそれぞれが正しい計算結果を返すこと(四則演算の正常系)
  • (3 + 5) * 2 - 4 = 12 など、入れ子になった式が正しく評価されること(複合式の検証)
  • 除算の右辺が 0 のとき ArithmeticException がスローされること(境界値: ゼロ除算)
  • 負の数を含む式が正しく評価されること(例: NumberExpression(-5)
  • 同じ式インスタンスを複数回 interpret() しても同じ結果になること(冪等性)
  • 式ツリーの一部を別の式に組み込んで再利用できること

GitHub でソースコードを見る →