DP-15: Interpreter パターン
特定の言語や文法をクラス群で表現し、文を解釈・評価するパターンです。 「式」をクラスで表し、それらを木構造(式ツリー)に組み合わせて複雑な表現を解釈します。
Interpreter パターンとは
Interpreter(インタープリター)パターンは、特定の言語や文法に従った「文(sentence)」を 解釈・評価するパターンです。文法の各規則をクラスとして表現し、それらを組み合わせて 複雑な式を構築します。
登場する役割
- AbstractExpression(抽象式):
interpret()メソッドを定義するインターフェース - TerminalExpression(終端式): それ以上分解できない最小単位の式(例: 数値リテラル)
- NonterminalExpression(非終端式): 他の式を組み合わせた複合式(例: 加算、乗算)
- Context: インタープリターが参照するグローバルな情報(変数テーブルなど)
主なユースケースとして、設定ファイルの式評価(例: フィルター条件の文字列を解析する)、 SQL の WHERE 句のような条件式の評価、テンプレートエンジンの変数展開などがあります。 標準ライブラリでは java.util.regex.Pattern(正規表現)が Interpreter パターンに基づいた設計の代表例です。
下のサンプルでは、四則演算(加算・減算・乗算・除算)の簡易インタープリターを実装します。(3 + 5) * 2 - 4 のような式をクラスの木構造として表現し、interpret() を再帰的に呼び出して評価します。
サンプルコード
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()しても同じ結果になること(冪等性) - 式ツリーの一部を別の式に組み込んで再利用できること