One-line Definition
Translates a grammar rule into a class, allowing you to parse and evaluate simple domain-specific languages using an expression tree.
Problem
Suppose your application allows users to filter products using a custom query string like price > 100 AND category = "electronics". Writing a massive string parsing function with dozens of nested if-else statements is fragile, impossible to maintain, and extremely hard to extend. The Interpreter pattern solves this by mapping each rule of the query language (e.g., "AND", "GreaterThan") to its own class, forming an easily traversable expression tree.
Real-world Analogy
Think of Roman numerals. When you read "XIV", you apply rules: X = 10, I = 1, V = 5, and "I before V" means subtract. Each symbol is interpreted according to a simple grammar. An interpreter applies these rules character by character to produce the number 14.
Structure
- Client
- Builds the abstract syntax tree (AST) out of terminal and non-terminal expressions, then calls
interpret().
- Builds the abstract syntax tree (AST) out of terminal and non-terminal expressions, then calls
- Context
- Contains global information or state the interpreter needs during evaluation (e.g., variables, environments).
- AbstractExpression
- Interface declaring the
interpret()method.
- Interface declaring the
- TerminalExpression
- Leaf nodes of the AST. Handles the simplest elements of the grammar (e.g., numbers, variables).
- NonterminalExpression
- Branch nodes of the AST. Composes other expressions and recursively evaluates them (e.g., addition, subtraction).
Diagram
classDiagram
class Client {
}
class Context {
}
class Expression {
<<interface>>
+interpret(Context)
}
class TerminalExpression {
+interpret(Context)
}
class NonterminalExpression {
-Expression left
-Expression right
+interpret(Context)
}
Client --> Expression : calls
Client --> Context : creates
TerminalExpression ..|> Expression
NonterminalExpression ..|> Expression
NonterminalExpression o-- Expression : composes
Code Walkthrough
Notice how the complex expression (5 + 3) * (10 - 2) is broken down into an Object Tree. When interpret() is called on the root, it recursively evaluates the entire tree.
interface Expression {
int interpret();
}
class NumberExpression implements Expression {
private final int number;
public NumberExpression(int number) {
this.number = number;
}
@Override
public int interpret() { return number; }
}
class AddExpression implements Expression {
private final Expression left;
private final Expression right;
public AddExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret() { return left.interpret() + right.interpret(); }
}
class SubtractExpression implements Expression {
private final Expression left;
private final Expression right;
public SubtractExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret() { return left.interpret() - right.interpret(); }
}
class MultiplyExpression implements Expression {
private final Expression left;
private final Expression right;
public MultiplyExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret() { return left.interpret() * right.interpret(); }
}
class Main {
public static void main(String[] args) {
// Tree representing: (5 + 3) * (10 - 2) = 64
Expression five = new NumberExpression(5);
Expression three = new NumberExpression(3);
Expression ten = new NumberExpression(10);
Expression two = new NumberExpression(2);
Expression sum = new AddExpression(five, three); // 5 + 3 = 8
Expression diff = new SubtractExpression(ten, two); // 10 - 2 = 8
Expression result = new MultiplyExpression(sum, diff); // 8 * 8 = 64
System.out.println("Result: " + result.interpret());
}
}
Bad vs Good
Bad Approach
Problems
- Parsing and evaluation logic is mixed together into a giant string-parsing method.
- Fails catastrophically on nested expressions or when operator precedence matters.
class Calculator {
public int evaluate(String expression) {
// Extremely fragile parsing logic
String[] parts = expression.split(" ");
int left = Integer.parseInt(parts[0]);
String operator = parts[1];
int right = Integer.parseInt(parts[2]);
if (operator.equals("+")) return left + right;
if (operator.equals("-")) return left - right;
throw new IllegalArgumentException("Unknown operator");
}
}
Better Approach
Improvements
- Each operator is isolated into its own class.
- The grammar naturally builds into a tree, inherently solving nesting and precedence.
// Composable expression tree cleanly separates syntax from evaluation
Expression expr = new MultiplyExpression(
new AddExpression(new NumberExpression(5), new NumberExpression(3)),
new NumberExpression(2)
);
System.out.println(expr.interpret()); // 16
Pros vs Cons
| Pros | Cons |
|---|---|
| Maps grammar rules to classes, making languages easy to implement | Severe class explosion for complex grammars |
| Easily extended by adding new Expression classes | Poor performance for large or complex languages |
| Expression trees are composable and highly reusable | Requires you to build a parser separately to construct the AST |
| Excellent for Domain-Specific Languages (DSLs) | Complex grammars are much better handled by tools like ANTLR |
When to Use
- You have a simple, recurring language to interpret (e.g., basic math expressions, query filters, rules engines).
- The grammar is simple and unlikely to change drastically.
- Efficiency is not the primary concern.
When Not to Use
- The grammar is complex (e.g., parsing actual programming languages like Java or Python).
- You need high performance (interpreted trees are slow compared to compiled byte-code).
Real-world Examples
java.util.regex.Pattern(Compiles regular expressions into an internal tree structure).java.text.Formatand its subclasses (e.g.,MessageFormat).- Spring Expression Language (SpEL) internally uses AST interpretation.
Key Takeaway
The Interpreter Pattern provides an elegant object-oriented way to evaluate simple Domain Specific Languages by treating every rule as a class. Use it for small rule engines or custom query languages, but immediately switch to a dedicated parser generator (like ANTLR or JavaCC) if the grammar grows beyond a handful of rules.