概要
業務ルールや計算式をコードに直接埋め込むのではなく、文法規則として構造化する。Interpreter パターンは、四則演算や論理式の評価、設定ファイルの簡易パーサーなど、小さな言語を扱う場面で有効です。構文木の各ノードが自身の評価ロジックを持ち、再帰的に式全体を計算する仕組みは、コンパイラの教科書的な構成でもあります。ただし文法が複雑になると Expression クラスが膨れるため、適用範囲は限られます。この記事では、四則演算の式ツリーを題材に、終端式・非終端式の構造をクラスとして実装します。Java 17 の sealed interface + record で式の種類を型安全に限定し、Java 21 の switch パターンマッチングで評価ロジックを外出しにする手法も確認します。
使いどころ
帳票テンプレートに埋め込まれた計算式(例: 「単価 * 数量 - 値引額」)を式ツリーに変換して動的に評価する
ワークフローの条件分岐ルール(例: 「金額 > 10000 AND 部門 = 営業」)を論理式として解釈し、承認要否を判定する
設定ファイル中の簡易DSL(独自記法)をパースし、処理パラメータとして評価する
コード例
public class InterpreterPatternDemo {
sealed interface Expression
permits NumberExpression, AddExpression,
SubtractExpression, MultiplyExpression,
DivideExpression {
int interpret();
}
record NumberExpression(int value) implements Expression {
@Override public int interpret() { return value; }
}
record AddExpression(Expression left, Expression right)
implements Expression {
@Override public int interpret() {
return left.interpret() + right.interpret();
}
}
record SubtractExpression(Expression left, Expression right)
implements Expression {
@Override public int interpret() {
return left.interpret() - right.interpret();
}
}
record MultiplyExpression(Expression left, Expression right)
implements Expression {
@Override public int interpret() {
return left.interpret() * right.interpret();
}
}
record DivideExpression(Expression left, Expression right)
implements Expression {
@Override public int interpret() {
int divisor = right.interpret();
if (divisor == 0) {
throw new ArithmeticException("ゼロ除算");
}
return left.interpret() / divisor;
}
}
static String toFormula(Expression expr) {
if (expr instanceof NumberExpression num) {
return String.valueOf(num.value());
} else if (expr instanceof AddExpression add) {
return "(" + toFormula(add.left()) + " + "
+ toFormula(add.right()) + ")";
} else if (expr instanceof SubtractExpression sub) {
return "(" + toFormula(sub.left()) + " - "
+ toFormula(sub.right()) + ")";
} else if (expr instanceof MultiplyExpression mul) {
return "(" + toFormula(mul.left()) + " * "
+ toFormula(mul.right()) + ")";
} else if (expr instanceof DivideExpression div) {
return "(" + toFormula(div.left()) + " / "
+ toFormula(div.right()) + ")";
}
return "?";
}
public static void main(String[] args) {
// (3 + 5) * 2 - 4 = 12
var expr = new SubtractExpression(
new MultiplyExpression(
new AddExpression(
new NumberExpression(3), new NumberExpression(5)),
new NumberExpression(2)),
new NumberExpression(4));
System.out.println("式: " + toFormula(expr));
System.out.println("結果: " + expr.interpret());
// 式の再利用: base = 5 + 3 を2倍
var base = new AddExpression(
new NumberExpression(5), new NumberExpression(3));
var doubled = new MultiplyExpression(base,
new NumberExpression(2));
System.out.println("base = " + toFormula(base)
+ " = " + base.interpret());
System.out.println("doubled = " + toFormula(doubled)
+ " = " + doubled.interpret());
}
}Version Coverage
sealed interface + record で式の種類を限定でき、instanceof パターンマッチングでキャスト不要になる。
// Java 17: sealed interface + record で型安全に
sealed interface Expression
permits NumberExpression, AddExpression,
SubtractExpression, MultiplyExpression,
DivideExpression {
int interpret();
}
record NumberExpression(int value) implements Expression {
@Override public int interpret() { return value; }
}
// instanceof パターンマッチングでキャスト不要
if (expr instanceof NumberExpression num) {
return String.valueOf(num.value());
}Library Comparison
注意点
Interpreter パターンは文法規則が10種類を超えると管理しにくくなる。複雑な文法にはパーサージェネレーターやスクリプトエンジンを検討する
式ツリーの深さに上限を設けないと、再帰呼び出しで StackOverflowError が発生する可能性がある
ゼロ除算や型不一致など、評価時の例外処理を各 Expression で確実に行うこと。特に DivideExpression は right.interpret() == 0 のガードが必須
sealed interface の permits リストに新しい式を追加すると、switch 文の網羅チェックでコンパイルエラーになる。これは安全性の面ではメリットだが、拡張頻度が高い場合は設計を見直す
FAQ
Interpreter は各ノードが自身の評価ロジックを持ちます。Visitor は評価ロジックをノードの外に分離し、複数の走査処理を追加しやすくします。式の種類が固定で処理を増やしたい場合は Visitor が有利です。
interpret の戻り値をジェネリクスや Object にする方法もありますが、型安全性が落ちます。sealed interface で Expression<T> とするか、結果を Value record でラップする方が安全です。
直接使う機会は多くありませんが、計算式や条件式の動的評価、テンプレートエンジンの内部構造など、知っておくと設計の引き出しが広がります。