Introduction to the Interpreter Pattern
The Interpreter pattern is a behavioral design pattern that defines a grammatical representation for a language and provides an interpreter to process sentences in that language. It's particularly useful when you need to evaluate or parse simple domain-specific languages (DSLs) or expressions.
The Problem
Imagine you're building a logistics system that needs to handle various address formatting requirements. Different regions and use cases require addresses to be formatted differently:
- US Standard:
{street}, {city}, {state} {zip} - Compact:
{street}, {city} {zip} - Full:
{street}, {city}, {state} {zip}, {country} - Label:
{name}\n{street}\n{city}, {state} {zip}
You could hard-code each format variation, but what happens when:
- You need to add new formats frequently
- Users want to define custom formats
- You need to validate format strings before use
- You need to parse and extract format components
Hard-coding becomes unmaintainable. You need a way to define a grammar for these format strings and interpret them to produce the desired output.
The Solution
The Interpreter pattern solves this by:
- Defining a grammar - Establishing the syntax rules for your language (in our case, format strings with placeholders)
- Representing grammar as classes - Each grammar rule becomes a class
- Building an expression tree - Parsing creates a tree of expression objects
- Interpreting the tree - Walking the tree evaluates the expression
Here's a simple example of how an address format expression is interpreted:
typescript1// Format string: "{street}, {city} {zip}"2// Becomes an expression tree:34ConcatExpression([5 VariableExpression("street"),6 LiteralExpression(", "),7 VariableExpression("city"),8 LiteralExpression(" "),9 VariableExpression("zip")10])1112// Interpretation with context { street: "123 Main St", city: "Boston", zip: "02101" }:13// → "123 Main St, Boston 02101"
Pattern Structure
The Interpreter pattern consists of four main components:
1. AbstractExpression
The interface that declares an interpret() method. All expression classes implement this interface.
typescript1interface Expression {2 interpret(context: Context): string;3}
2. TerminalExpression
Implements interpret for terminal symbols in the grammar - the "leaves" of the expression tree that don't contain other expressions.
typescript1// Terminal: a literal string2class LiteralExpression implements Expression {3 constructor(private value: string) {}45 interpret(context: Context): string {6 return this.value;7 }8}910// Terminal: a variable lookup11class VariableExpression implements Expression {12 constructor(private name: string) {}1314 interpret(context: Context): string {15 return context.get(this.name);16 }17}
3. NonterminalExpression
Implements interpret for nonterminal symbols - expressions that contain other expressions (branches of the tree).
typescript1// Nonterminal: concatenate multiple expressions2class ConcatExpression implements Expression {3 constructor(private expressions: Expression[]) {}45 interpret(context: Context): string {6 return this.expressions7 .map(expr => expr.interpret(context))8 .join('');9 }10}
4. Context
Holds the global state needed for interpretation - typically variable values or configuration.
typescript1class Context {2 constructor(private variables: Map<string, string>) {}34 get(name: string): string {5 return this.variables.get(name) || '';6 }7}
How It Works: Address Format Example
Let's see how these components work together to parse and interpret an address format:
typescript1// Step 1: Parse format string into expression tree2const formatString = "{street}, {city} {zip}";3const expression = parseFormat(formatString);4// Creates: ConcatExpression([5// VariableExpression("street"),6// LiteralExpression(", "),7// VariableExpression("city"),8// LiteralExpression(" "),9// VariableExpression("zip")10// ])1112// Step 2: Create context with address data13const context = new Context(new Map([14 ['street', '123 Main St'],15 ['city', 'Boston'],16 ['state', 'MA'],17 ['zip', '02101']18]));1920// Step 3: Interpret the expression21const result = expression.interpret(context);22console.log(result); // "123 Main St, Boston 02101"
Logistics Domain Context
In logistics systems, the Interpreter pattern is valuable for:
Address Formatting
Parse and evaluate format templates for different output requirements:
typescript1const formats = {2 shipping: "{name}\n{street}\n{city}, {state} {zip}",3 billing: "{street}, {city}, {state} {zip}",4 compact: "{city}, {state}"5};
Route Expression Language
Define simple DSLs for route specifications:
typescript1// Route: "WAREHOUSE -> STORE(priority=high) -> CUSTOMER"2// Interpreted to create route objects with constraints
Condition Evaluation
Parse and evaluate business rules:
typescript1// Rule: "weight > 50 AND destination = 'international'"2// Interpreted to filter shipments
Label Template System
Allow users to define custom shipping label layouts:
typescript1// Template: "{tracking}\n{from.city} → {to.city}\n{service}"2// Interpreted to generate label content
Benefits
- Easy to change and extend grammar - Add new expression types without modifying existing code
- Grammar is explicit - The class structure directly represents the grammar rules
- Flexible interpretation - Different contexts can produce different results from the same expression
- Testable - Each expression type can be tested independently
Drawbacks
- Complex grammars are hard to maintain - Each grammar rule requires a new class
- Not efficient for complex languages - Better to use parser generators (ANTLR, PEG.js)
- Expression tree overhead - Creating and traversing trees has performance cost
- Limited to simple languages - Works best for simple DSLs, not full programming languages
When to Use Interpreter Pattern
Use the Interpreter pattern when:
- You have a simple language or DSL to interpret
- Grammar is stable but you need to evaluate many different sentences
- Efficiency is not critical - interpretation is slower than compiled approaches
- You want grammar rules as classes for clarity and extensibility
Don't use it when:
- The language is complex - use a proper parser library instead
- Performance is critical - compiled or cached approaches are faster
- The grammar changes frequently - class explosion becomes unmaintainable
Real-World Examples
- Regular expressions - Each regex component is an expression
- SQL interpreters - Simple query languages
- Template engines - Mustache, Handlebars for simple templates
- Mathematical expression evaluators - Calculator parsers
- Configuration DSLs - Simple configuration languages
Summary
The Interpreter pattern provides a way to define and evaluate sentences in a simple language by:
- Representing grammar rules as classes (AbstractExpression)
- Implementing terminal expressions (literals, variables)
- Implementing nonterminal expressions (compositions, operations)
- Using context to provide interpretation state
- Building and traversing expression trees
In our logistics context, it's perfect for address format parsing, allowing flexible template-based formatting without hard-coding every variation.
Next, we'll implement the Interpreter pattern to build a complete address format parser.