Interpreter Pattern

The Interpreter Pattern is a behavioral design pattern that defines a grammatical representation for a language and provides an interpreter to evaluate sentences in that language. It's particularly useful when you need to interpret expressions, parse domain-specific languages (DSLs), or evaluate rules repeatedly.
The Problem It Solves
Imagine you're building a system where users need to define custom rules, filters, or expressions β and these need to be evaluated repeatedly at runtime.
Without the Interpreter Pattern:
You end up with massive
if-elseorswitchchainsAdding new operations requires modifying existing code (violates Open/Closed Principle)
Complex expressions become impossible to maintain
No clean way to represent nested or recursive rules
// β The messy approach without Interpreter Pattern
public boolean evaluate(String expression, Map<String, Object> data) {
if (expression.contains("AND")) {
String[] parts = expression.split("AND");
// Parse left side... parse right side... handle nesting???
// What about OR? What about NOT? What about parentheses?
// This quickly becomes unmaintainable! π±
}
// ... hundreds of lines of brittle parsing logic
}
With the Interpreter Pattern:
Each grammar rule is a self-contained class
New operations = new classes (no modification to existing code)
Expressions are represented as composable tree structures
Easy to extend, test, and maintain
// β
Clean approach with Interpreter Pattern
Expression query = new AndExpression(
new GreaterThanExpression("age", 25),
new EqualsExpression("status", "active")
);
boolean result = query.interpret(context); // Simple and elegant!
When to Use the Interpreter Pattern
Repeated evaluation of similar expressions or rules
Domain-specific languages (DSLs) for configuration, queries, or business rules
Mathematical expression evaluation
SQL-like query parsing
Regular expression matching
Boolean logic evaluation
Structure
Key Components
| Component | Role |
|---|---|
| AbstractExpression | Declares the interpret() method common to all nodes |
| TerminalExpression | Implements interpretation for terminal symbols (leaves) |
| NonTerminalExpression | Implements interpretation for grammar rules (composites) |
| Context | Contains global information needed during interpretation |
| Client | Builds the abstract syntax tree and invokes interpretation |
Example 1: Boolean Expression Evaluator
Let's build an interpreter that evaluates boolean expressions like (TRUE AND FALSE) OR TRUE.
Expression Tree Visualization
Implementation
// Abstract Expression
public interface Expression {
boolean interpret();
}
// Terminal Expression - represents a boolean constant
public class TerminalExpression implements Expression {
private final boolean value;
public TerminalExpression(boolean value) {
this.value = value;
}
@Override
public boolean interpret() {
return value;
}
}
// Non-Terminal Expression - AND operation
public class AndExpression implements Expression {
private final Expression left;
private final Expression right;
public AndExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public boolean interpret() {
return left.interpret() && right.interpret();
}
}
// Non-Terminal Expression - OR operation
public class OrExpression implements Expression {
private final Expression left;
private final Expression right;
public OrExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public boolean interpret() {
return left.interpret() || right.interpret();
}
}
// Non-Terminal Expression - NOT operation
public class NotExpression implements Expression {
private final Expression expression;
public NotExpression(Expression expression) {
this.expression = expression;
}
@Override
public boolean interpret() {
return !expression.interpret();
}
}
Client Usage
public class BooleanExpressionDemo {
public static void main(String[] args) {
// Expression: (TRUE AND FALSE) OR TRUE
Expression trueExpr = new TerminalExpression(true);
Expression falseExpr = new TerminalExpression(false);
Expression andExpr = new AndExpression(trueExpr, falseExpr);
Expression orExpr = new OrExpression(andExpr, new TerminalExpression(true));
System.out.println("(TRUE AND FALSE) OR TRUE = " + orExpr.interpret());
// Output: (TRUE AND FALSE) OR TRUE = true
// Expression: NOT (TRUE AND TRUE)
Expression notExpr = new NotExpression(
new AndExpression(
new TerminalExpression(true),
new TerminalExpression(true)
)
);
System.out.println("NOT (TRUE AND TRUE) = " + notExpr.interpret());
// Output: NOT (TRUE AND TRUE) = false
}
}
Example 2: Mathematical Expression Evaluator with Variables
A more practical example using a Context to store variables.
Flow Diagram
Implementation
import java.util.HashMap;
import java.util.Map;
// Context - stores variable bindings
public class Context {
private final Map<String, Integer> variables = new HashMap<>();
public void assign(String variable, int value) {
variables.put(variable, value);
}
public int lookup(String variable) {
if (!variables.containsKey(variable)) {
throw new IllegalArgumentException("Unknown variable: " + variable);
}
return variables.get(variable);
}
}
// Abstract Expression
public interface MathExpression {
int interpret(Context context);
}
// Terminal Expression - Number literal
public class NumberExpression implements MathExpression {
private final int number;
public NumberExpression(int number) {
this.number = number;
}
@Override
public int interpret(Context context) {
return number;
}
}
// Terminal Expression - Variable reference
public class VariableExpression implements MathExpression {
private final String name;
public VariableExpression(String name) {
this.name = name;
}
@Override
public int interpret(Context context) {
return context.lookup(name);
}
}
// Non-Terminal Expression - Addition
public class AddExpression implements MathExpression {
private final MathExpression left;
private final MathExpression right;
public AddExpression(MathExpression left, MathExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
return left.interpret(context) + right.interpret(context);
}
}
// Non-Terminal Expression - Subtraction
public class SubtractExpression implements MathExpression {
private final MathExpression left;
private final MathExpression right;
public SubtractExpression(MathExpression left, MathExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
return left.interpret(context) - right.interpret(context);
}
}
// Non-Terminal Expression - Multiplication
public class MultiplyExpression implements MathExpression {
private final MathExpression left;
private final MathExpression right;
public MultiplyExpression(MathExpression left, MathExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
return left.interpret(context) * right.interpret(context);
}
}
Client Usage
public class MathExpressionDemo {
public static void main(String[] args) {
// Expression: (x + y) * 2 - z
// Where x=10, y=5, z=3
Context context = new Context();
context.assign("x", 10);
context.assign("y", 5);
context.assign("z", 3);
// Build the expression tree
MathExpression x = new VariableExpression("x");
MathExpression y = new VariableExpression("y");
MathExpression z = new VariableExpression("z");
MathExpression two = new NumberExpression(2);
// (x + y)
MathExpression sum = new AddExpression(x, y);
// (x + y) * 2
MathExpression product = new MultiplyExpression(sum, two);
// (x + y) * 2 - z
MathExpression result = new SubtractExpression(product, z);
System.out.println("(x + y) * 2 - z = " + result.interpret(context));
// Output: (x + y) * 2 - z = 27
// Change variable values and re-evaluate
context.assign("x", 20);
System.out.println("With x=20: " + result.interpret(context));
// Output: With x=20: 47
}
}
Example 3: SQL-like Query Interpreter
A practical example for filtering data using a simple query language.
Query Structure
Implementation
import java.util.Map;
// Context - represents a data row
public class Row {
private final Map<String, Object> data;
public Row(Map<String, Object> data) {
this.data = data;
}
public Object get(String column) {
return data.get(column);
}
}
// Abstract Expression for query conditions
public interface QueryExpression {
boolean evaluate(Row row);
}
// Terminal Expression - Column equals value
public class EqualsExpression implements QueryExpression {
private final String column;
private final Object value;
public EqualsExpression(String column, Object value) {
this.column = column;
this.value = value;
}
@Override
public boolean evaluate(Row row) {
Object columnValue = row.get(column);
return value.equals(columnValue);
}
}
// Terminal Expression - Column greater than value
public class GreaterThanExpression implements QueryExpression {
private final String column;
private final Comparable<?> value;
public GreaterThanExpression(String column, Comparable<?> value) {
this.column = column;
this.value = value;
}
@Override
@SuppressWarnings("unchecked")
public boolean evaluate(Row row) {
Comparable<Object> columnValue = (Comparable<Object>) row.get(column);
return columnValue.compareTo(value) > 0;
}
}
// Terminal Expression - Column less than value
public class LessThanExpression implements QueryExpression {
private final String column;
private final Comparable<?> value;
public LessThanExpression(String column, Comparable<?> value) {
this.column = column;
this.value = value;
}
@Override
@SuppressWarnings("unchecked")
public boolean evaluate(Row row) {
Comparable<Object> columnValue = (Comparable<Object>) row.get(column);
return columnValue.compareTo(value) < 0;
}
}
// Non-Terminal Expression - AND
public class AndQueryExpression implements QueryExpression {
private final QueryExpression left;
private final QueryExpression right;
public AndQueryExpression(QueryExpression left, QueryExpression right) {
this.left = left;
this.right = right;
}
@Override
public boolean evaluate(Row row) {
return left.evaluate(row) && right.evaluate(row);
}
}
// Non-Terminal Expression - OR
public class OrQueryExpression implements QueryExpression {
private final QueryExpression left;
private final QueryExpression right;
public OrQueryExpression(QueryExpression left, QueryExpression right) {
this.left = left;
this.right = right;
}
@Override
public boolean evaluate(Row row) {
return left.evaluate(row) || right.evaluate(row);
}
}
Client Usage
import java.util.*;
import java.util.stream.Collectors;
public class QueryDemo {
public static void main(String[] args) {
// Sample data
List<Row> employees = Arrays.asList(
new Row(Map.of("name", "Alice", "age", 30, "department", "Engineering", "salary", 85000)),
new Row(Map.of("name", "Bob", "age", 24, "department", "Marketing", "salary", 55000)),
new Row(Map.of("name", "Charlie", "age", 35, "department", "Engineering", "salary", 95000)),
new Row(Map.of("name", "Diana", "age", 28, "department", "HR", "salary", 60000)),
new Row(Map.of("name", "Eve", "age", 32, "department", "Engineering", "salary", 90000))
);
// Query: age > 25 AND department = 'Engineering'
QueryExpression query = new AndQueryExpression(
new GreaterThanExpression("age", 25),
new EqualsExpression("department", "Engineering")
);
System.out.println("Query: age > 25 AND department = 'Engineering'");
List<Row> results = employees.stream()
.filter(query::evaluate)
.collect(Collectors.toList());
results.forEach(row -> System.out.println(" - " + row.get("name")));
// Output:
// - Alice
// - Charlie
// - Eve
// Query: salary >= 80000 OR age < 25
QueryExpression query2 = new OrQueryExpression(
new GreaterThanExpression("salary", 79999),
new LessThanExpression("age", 25)
);
System.out.println("\nQuery: salary >= 80000 OR age < 25");
employees.stream()
.filter(query2::evaluate)
.forEach(row -> System.out.println(" - " + row.get("name")));
// Output:
// - Alice
// - Bob
// - Charlie
// - Eve
}
}
Interpreter Pattern with Parser
In real-world applications, you typically need a parser to convert string input into an expression tree.
Parsing Flow
Simple Expression Parser
import java.util.*;
import java.util.regex.*;
public class ExpressionParser {
private final List<String> tokens;
private int position = 0;
public ExpressionParser(String expression) {
this.tokens = tokenize(expression);
}
private List<String> tokenize(String expression) {
List<String> tokens = new ArrayList<>();
Matcher matcher = Pattern.compile("\\d+|[+\\-*/()]").matcher(expression);
while (matcher.find()) {
tokens.add(matcher.group());
}
return tokens;
}
public MathExpression parse() {
return parseExpression();
}
private MathExpression parseExpression() {
MathExpression left = parseTerm();
while (position < tokens.size()) {
String operator = tokens.get(position);
if (!operator.equals("+") && !operator.equals("-")) {
break;
}
position++;
MathExpression right = parseTerm();
if (operator.equals("+")) {
left = new AddExpression(left, right);
} else {
left = new SubtractExpression(left, right);
}
}
return left;
}
private MathExpression parseTerm() {
MathExpression left = parseFactor();
while (position < tokens.size()) {
String operator = tokens.get(position);
if (!operator.equals("*") && !operator.equals("/")) {
break;
}
position++;
MathExpression right = parseFactor();
if (operator.equals("*")) {
left = new MultiplyExpression(left, right);
} else {
left = new DivideExpression(left, right);
}
}
return left;
}
private MathExpression parseFactor() {
String token = tokens.get(position);
if (token.equals("(")) {
position++; // consume '('
MathExpression expr = parseExpression();
position++; // consume ')'
return expr;
}
position++;
return new NumberExpression(Integer.parseInt(token));
}
}
// Usage
public class ParserDemo {
public static void main(String[] args) {
String expression = "3 + 5 * 2";
ExpressionParser parser = new ExpressionParser(expression);
MathExpression ast = parser.parse();
Context context = new Context();
System.out.println(expression + " = " + ast.interpret(context));
// Output: 3 + 5 * 2 = 13
expression = "(3 + 5) * 2";
parser = new ExpressionParser(expression);
ast = parser.parse();
System.out.println(expression + " = " + ast.interpret(context));
// Output: (3 + 5) * 2 = 16
}
}
Advantages and Disadvantages
Real-World Applications
| Application | Example |
|---|---|
| Regular Expressions | java.util.regex.Pattern |
| SQL Parsing | JDBC query parsing, JPA Criteria API |
| Expression Languages | Spring Expression Language (SpEL), OGNL |
| Template Engines | Thymeleaf, FreeMarker expression parsing |
| Rule Engines | Drools, Easy Rules |
| Mathematical Software | Calculator apps, formula evaluators |
| Configuration DSLs | Build tools (Gradle), CI/CD pipelines |
Interpreter vs Other Patterns
| Pattern | Use When |
|---|---|
| Interpreter | You need to evaluate a language grammar |
| Visitor | You want to add operations without modifying classes |
| Composite | You need uniform treatment of individual and composite objects |
| Strategy | You want to swap algorithms at runtime |
Best Practices
Keep grammars simple β Use parser generators (ANTLR, JavaCC) for complex grammars
Use Flyweight β Share terminal expressions to reduce memory usage
Consider caching β Cache interpretation results for repeated evaluations
Combine with Composite β The expression tree naturally follows the Composite pattern
Separate parsing from interpretation β Keep the parser and interpreter loosely coupled
Summary
The Interpreter Pattern provides a powerful way to define and evaluate domain-specific languages. While it shines for simple grammars and repeated evaluations, consider dedicated parsing tools for complex language implementations.
Key Takeaways:
Use for simple, well-defined grammars
Each grammar rule becomes a class
The Context holds global state during interpretation
Combine with Composite for tree structures
Consider parser generators for complex grammars
The Interpreter Pattern transforms complex rule evaluation into a clean, extensible object-oriented design β perfect for DSLs, expression evaluators, and configuration languages.
Why did the Interpreter Pattern break up with the Compiler?
"You're too committed β I prefer to evaluate things one expression at a time!"



