Behavioral

Interpreter Design Pattern

Define a grammar and interpret sentences in a language

The Interpreter Pattern: Building Your Own Mini Language

Ever written a formula in Excel? Ever used regular expressions? Ever configured routes in a web framework? You’ve used languages interpreted at runtime. That’s the Interpreter pattern.

The idea: define a grammar for a language and build an interpreter that processes sentences in that language.

The Problem: Hardcoded Logic for Expressions

You’re building a calculator that needs to evaluate expressions:

class Calculator {
    public int evaluate(String expression) {
        if (expression.equals("5 + 3")) {
            return 8;
        } else if (expression.equals("10 - 2")) {
            return 8;
        } else if (expression.equals("4 * 2")) {
            return 8;
        }
        // Add every possible expression? Impossible!
    }
}

Problems:

Can’t handle new expressions: Need to hardcode every possible expression.

Not scalable: Can’t add new operations without modifying code.

No grammar: Can’t define rules for valid expressions.

Not flexible: Can’t combine operations like “(5 + 3) * 2”.

What if we could define a grammar and build an interpreter to evaluate any expression?

The Interpreter Solution

Define a grammar and create classes to interpret it:

// Abstract expression
interface Expression {
    int interpret();
}

// Terminal expression: number
class NumberExpression implements Expression {
    private int number;
    
    public NumberExpression(int number) {
        this.number = number;
    }
    
    @Override
    public int interpret() {
        return number;
    }
}

// Non-terminal expression: addition
class AddExpression implements Expression {
    private Expression left;
    private Expression right;
    
    public AddExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }
    
    @Override
    public int interpret() {
        return left.interpret() + right.interpret();
    }
}

// Non-terminal expression: subtraction
class SubtractExpression implements Expression {
    private Expression left;
    private Expression right;
    
    public SubtractExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }
    
    @Override
    public int interpret() {
        return left.interpret() - right.interpret();
    }
}

// Non-terminal expression: multiplication
class MultiplyExpression implements Expression {
    private Expression left;
    private Expression right;
    
    public MultiplyExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }
    
    @Override
    public int interpret() {
        return left.interpret() * right.interpret();
    }
}

Usage:

// Build expression tree for: (5 + 3) * 2
Expression expression = new MultiplyExpression(
    new AddExpression(
        new NumberExpression(5),
        new NumberExpression(3)
    ),
    new NumberExpression(2)
);

int result = expression.interpret();
System.out.println("Result: " + result);  // 16

The expression is represented as a tree. Calling interpret() evaluates it recursively.

The Components

1. Abstract Expression

Declares interpret operation:

interface Expression {
    int interpret();
}

2. Terminal Expression

Implements interpret for terminal symbols (leaves):

class NumberExpression implements Expression {
    private int value;
    
    public int interpret() {
        return value;
    }
}

3. Non-Terminal Expression

Implements interpret for grammar rules (operators):

class AddExpression implements Expression {
    private Expression left;
    private Expression right;
    
    public int interpret() {
        return left.interpret() + right.interpret();
    }
}

4. Context (Optional)

Holds global information:

class Context {
    private Map<String, Integer> variables;
    
    public void setVariable(String name, int value) {
        variables.put(name, value);
    }
    
    public int getVariable(String name) {
        return variables.get(name);
    }
}

Real-World Example: Boolean Expression Evaluator

// Abstract expression
interface BooleanExpression {
    boolean interpret(Context context);
}

// Terminal: variable
class VariableExpression implements BooleanExpression {
    private String name;
    
    public VariableExpression(String name) {
        this.name = name;
    }
    
    @Override
    public boolean interpret(Context context) {
        return context.lookup(name);
    }
}

// Terminal: constant
class ConstantExpression implements BooleanExpression {
    private boolean value;
    
    public ConstantExpression(boolean value) {
        this.value = value;
    }
    
    @Override
    public boolean interpret(Context context) {
        return value;
    }
}

// Non-terminal: AND
class AndExpression implements BooleanExpression {
    private BooleanExpression left;
    private BooleanExpression right;
    
    public AndExpression(BooleanExpression left, BooleanExpression right) {
        this.left = left;
        this.right = right;
    }
    
    @Override
    public boolean interpret(Context context) {
        return left.interpret(context) && right.interpret(context);
    }
}

// Non-terminal: OR
class OrExpression implements BooleanExpression {
    private BooleanExpression left;
    private BooleanExpression right;
    
    public OrExpression(BooleanExpression left, BooleanExpression right) {
        this.left = left;
        this.right = right;
    }
    
    @Override
    public boolean interpret(Context context) {
        return left.interpret(context) || right.interpret(context);
    }
}

// Non-terminal: NOT
class NotExpression implements BooleanExpression {
    private BooleanExpression expression;
    
    public NotExpression(BooleanExpression expression) {
        this.expression = expression;
    }
    
    @Override
    public boolean interpret(Context context) {
        return !expression.interpret(context);
    }
}

// Context
class Context {
    private Map<String, Boolean> variables = new HashMap<>();
    
    public void assign(String name, boolean value) {
        variables.put(name, value);
    }
    
    public boolean lookup(String name) {
        return variables.getOrDefault(name, false);
    }
}

Usage:

Context context = new Context();
context.assign("x", true);
context.assign("y", false);

// Expression: (x AND NOT y) OR y
BooleanExpression expression = new OrExpression(
    new AndExpression(
        new VariableExpression("x"),
        new NotExpression(new VariableExpression("y"))
    ),
    new VariableExpression("y")
);

boolean result = expression.interpret(context);
System.out.println("Result: " + result);  // true

Another Example: SQL-Like Query Language

// Grammar:
// SELECT [columns] FROM [table] WHERE [condition]

interface QueryExpression {
    List<Map<String, Object>> interpret(Database db);
}

class SelectExpression implements QueryExpression {
    private List<String> columns;
    private String table;
    private ConditionExpression condition;
    
    public SelectExpression(List<String> columns, String table, 
                           ConditionExpression condition) {
        this.columns = columns;
        this.table = table;
        this.condition = condition;
    }
    
    @Override
    public List<Map<String, Object>> interpret(Database db) {
        List<Map<String, Object>> allRows = db.getTable(table);
        List<Map<String, Object>> filtered = new ArrayList<>();
        
        for (Map<String, Object> row : allRows) {
            if (condition == null || condition.evaluate(row)) {
                Map<String, Object> selectedRow = new HashMap<>();
                for (String column : columns) {
                    selectedRow.put(column, row.get(column));
                }
                filtered.add(selectedRow);
            }
        }
        
        return filtered;
    }
}

interface ConditionExpression {
    boolean evaluate(Map<String, Object> row);
}

class EqualsCondition implements ConditionExpression {
    private String column;
    private Object value;
    
    public EqualsCondition(String column, Object value) {
        this.column = column;
        this.value = value;
    }
    
    @Override
    public boolean evaluate(Map<String, Object> row) {
        return value.equals(row.get(column));
    }
}

class AndCondition implements ConditionExpression {
    private ConditionExpression left;
    private ConditionExpression right;
    
    public AndCondition(ConditionExpression left, ConditionExpression right) {
        this.left = left;
        this.right = right;
    }
    
    @Override
    public boolean evaluate(Map<String, Object> row) {
        return left.evaluate(row) && right.evaluate(row);
    }
}

Usage:

Database db = new Database();
// Populate database...

// SELECT name, age FROM users WHERE age = 25 AND active = true
QueryExpression query = new SelectExpression(
    Arrays.asList("name", "age"),
    "users",
    new AndCondition(
        new EqualsCondition("age", 25),
        new EqualsCondition("active", true)
    )
);

List<Map<String, Object>> results = query.interpret(db);

Parsing Strings into Expression Trees

Building expression trees manually is tedious. Usually you parse strings:

class ExpressionParser {
    public Expression parse(String input) {
        // Tokenize
        String[] tokens = input.split(" ");
        
        // Build expression tree
        Stack<Expression> stack = new Stack<>();
        Stack<String> operators = new Stack<>();
        
        for (String token : tokens) {
            if (isNumber(token)) {
                stack.push(new NumberExpression(Integer.parseInt(token)));
            } else if (isOperator(token)) {
                while (!operators.isEmpty() && 
                       precedence(operators.peek()) >= precedence(token)) {
                    Expression right = stack.pop();
                    Expression left = stack.pop();
                    String op = operators.pop();
                    stack.push(createExpression(op, left, right));
                }
                operators.push(token);
            }
        }
        
        while (!operators.isEmpty()) {
            Expression right = stack.pop();
            Expression left = stack.pop();
            String op = operators.pop();
            stack.push(createExpression(op, left, right));
        }
        
        return stack.pop();
    }
    
    private Expression createExpression(String op, Expression left, Expression right) {
        switch (op) {
            case "+": return new AddExpression(left, right);
            case "-": return new SubtractExpression(left, right);
            case "*": return new MultiplyExpression(left, right);
            default: throw new IllegalArgumentException("Unknown operator: " + op);
        }
    }
    
    private boolean isNumber(String token) {
        try {
            Integer.parseInt(token);
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }
    
    private boolean isOperator(String token) {
        return token.equals("+") || token.equals("-") || token.equals("*");
    }
    
    private int precedence(String op) {
        if (op.equals("*")) return 2;
        if (op.equals("+") || op.equals("-")) return 1;
        return 0;
    }
}

Usage:

ExpressionParser parser = new ExpressionParser();
Expression expr = parser.parse("5 + 3 * 2");
System.out.println(expr.interpret());  // 11

When to Use Interpreter

Use Interpreter when:

  • You have a simple grammar to interpret
  • Efficiency is not critical (interpreter can be slow)
  • Grammar doesn’t change often
  • You’re building a domain-specific language (DSL)

Real scenarios:

  • Configuration languages
  • Query languages
  • Rule engines
  • Expression evaluators
  • Regular expressions
  • Template engines
  • Math calculators
  • Command processors

When NOT to Use Interpreter

Don’t use Interpreter when:

  • Grammar is complex (use parser generators instead)
  • Performance is critical (compiled approaches are faster)
  • Grammar changes frequently (maintaining expression classes is tedious)

For complex grammars, consider:

  • ANTLR (parser generator)
  • JavaCC (Java Compiler Compiler)
  • Existing expression libraries

Interpreter in Real Frameworks

Spring Expression Language (SpEL)

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')");
String message = (String) exp.getValue();

Java Regex

Pattern pattern = Pattern.compile("[a-z]+");
Matcher matcher = pattern.matcher("hello");
// Regex engine interprets the pattern

SQL

Every database interprets SQL queries using the Interpreter pattern internally.

Common Pitfalls

Pitfall 1: Complex Grammar

// BAD: Too many expression classes for complex grammar
class Expression1 { }
class Expression2 { }
// ... 50 more expression classes

For complex grammars, use parser generators.

Pitfall 2: Mutable Expressions

// BAD: Reusing expressions with changing state
Expression expr = parser.parse("x + y");
context.set("x", 5);
expr.interpret(context);  // 5 + y
context.set("x", 10);
expr.interpret(context);  // 10 + y (reusing same expression)

Expressions should be immutable or context should be passed fresh each time.

Pitfall 3: No Caching

// BAD: Parsing same expression repeatedly
for (int i = 0; i < 1000; i++) {
    Expression expr = parser.parse("5 + 3");  // Parsing every time!
    expr.interpret();
}

// GOOD: Parse once, reuse
Expression expr = parser.parse("5 + 3");
for (int i = 0; i < 1000; i++) {
    expr.interpret();
}

Interpreter and Composite Pattern

Interpreter often uses Composite pattern:

Composite structure: Expression tree is a composite (non-terminals contain terminals).

Recursive interpretation: interpret() calls are recursive, like Composite operations.

Interpreter and SOLID Principles

Single Responsibility Principle

Each expression class handles one operation.

Open/Closed Principle

Add new expression types without modifying existing ones.

The Mental Model

Think of Interpreter like:

Math teacher evaluating expressions: Student writes “2 + 3 * 4”. Teacher breaks it into parts: “3 * 4 = 12”, then “2 + 12 = 14”. Each operation has rules.

Recipe instructions: Recipe says “Mix A and B, then add C.” Chef interprets each step. Each instruction (mix, add, bake) has meaning.

Music notation: Musicians read sheet music (grammar). Each symbol (notes, rests, dynamics) has meaning. Musicians interpret the score.

Performance Considerations

Interpreter can be slow:

  • Building expression tree takes time
  • Recursive interpretation has overhead
  • No optimization

Optimizations:

  • Cache parsed expressions
  • Compile to bytecode
  • Use visitor pattern for multiple operations
  • Consider JIT compilation for hot paths

Testing with Interpreter

@Test
public void testAddExpression() {
    Expression expr = new AddExpression(
        new NumberExpression(5),
        new NumberExpression(3)
    );
    assertEquals(8, expr.interpret());
}

@Test
public void testComplexExpression() {
    // (5 + 3) * 2
    Expression expr = new MultiplyExpression(
        new AddExpression(
            new NumberExpression(5),
            new NumberExpression(3)
        ),
        new NumberExpression(2)
    );
    assertEquals(16, expr.interpret());
}

@Test
public void testParser() {
    ExpressionParser parser = new ExpressionParser();
    Expression expr = parser.parse("5 + 3 * 2");
    assertEquals(11, expr.interpret());
}

Alternative: Visitor Pattern

For multiple operations on the same grammar, use Visitor:

interface ExpressionVisitor {
    int visitNumber(NumberExpression expr);
    int visitAdd(AddExpression expr);
    int visitMultiply(MultiplyExpression expr);
}

class EvaluationVisitor implements ExpressionVisitor {
    public int visitNumber(NumberExpression expr) {
        return expr.getValue();
    }
    
    public int visitAdd(AddExpression expr) {
        return expr.getLeft().accept(this) + expr.getRight().accept(this);
    }
    
    // ...
}

class PrintVisitor implements ExpressionVisitor {
    public int visitAdd(AddExpression expr) {
        System.out.println("Add operation");
        expr.getLeft().accept(this);
        expr.getRight().accept(this);
        return 0;
    }
    
    // ...
}

Visitor is better when you have many operations but stable grammar.

Final Thoughts

The Interpreter pattern is about defining a language and building an interpreter for it. It’s powerful for simple grammars but gets unwieldy for complex ones.

It’s not about showing off. It’s about:

  • Defining domain-specific languages
  • Evaluating expressions at runtime
  • Creating flexible rule engines
  • Building query languages

The key insight: represent grammar as classes. Interpret by traversing the structure.

Remember:

  • Terminal expressions are leaves
  • Non-terminal expressions are operations
  • Context holds global state
  • Keep grammar simple

Next time you need to evaluate expressions or rules at runtime, consider Interpreter. But know when to switch to parser generators for complex grammars.

Interpret wisely.