The Interpreter Pattern is a behavioral design pattern that defines a way to evaluate sentences or expressions in a language. It falls under the category of behavioral patterns because it addresses how objects and classes interact and distribute responsibilities. This pattern involves creating an interpreter to interpret sentences in a particular language.
The Interpreter Pattern is useful when you have a domain-specific language (DSL) or expressions that need to be interpreted. It provides a way to define a grammar for a language and then provides an interpreter that can interpret sentences in that language.
Important Components
- Abstract Expression: An interface or abstract class declaring an interpret() method that all concrete expressions implement.
- Terminal Expression: Represents the leaf nodes of the grammar (e.g., literals or basic elements) and implements the interpret() method to evaluate itself.
- Non-Terminal Expression: Represents composite expressions (e.g., operations combining other expressions) and implements the interpret() method by delegating to its child expressions.
- Context: Contains global information or state (e.g., variables, input data) used during interpretation.
- Client: Builds the abstract syntax tree (AST) from expressions and invokes the interpret() method.
How It Works
- The grammar of the language is represented as a hierarchy of expression classes (terminal and non-terminal).
- The Client constructs an abstract syntax tree (AST) by combining terminal and non-terminal expressions.
- The interpret() method is called on the root of the AST, which recursively evaluates the expression by delegating to child expressions.
- The Context provides external data (e.g., variable values) needed for evaluation.
- The pattern interprets the expression step-by-step, producing a result based on the grammar rules.
Sample Implementation
// Context
class Context {
private Map<String, Integer> variables;
public Context() {
variables = new HashMap<>();
}
public void setVariable(String name, Integer value) {
variables.put(name, value);
}
public Integer getVariable(String name) {
return variables.getOrDefault(name, 0);
}
}
// Abstract Expression
interface Expression {
int interpret(Context context);
}
// Terminal Expression
class NumberExpression implements Expression {
private String value; // Could be a number or variable name
public NumberExpression(String value) {
this.value = value;
}
@Override
public int interpret(Context context) {
try {
// Try parsing as a number
return Integer.parseInt(value);
} catch (NumberFormatException e) {
// If not a number, treat as a variable
return context.getVariable(value);
}
}
}
// Non-Terminal Expression for 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(Context context) {
return left.interpret(context) + right.interpret(context);
}
}
// Non-Terminal Expression for 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(Context context) {
return left.interpret(context) - right.interpret(context);
}
}
// Usage
public class Main {
public static void main(String[] args) {
// Create context with some variables
Context context = new Context();
context.setVariable("x", 10);
context.setVariable("y", 5);
// Build expression: (x + 3) - y
// Equivalent to: (10 + 3) - 5 = 8
Expression expression = new SubtractExpression(
new AddExpression(
new NumberExpression("x"),
new NumberExpression("3")
),
new NumberExpression("y")
);
// Interpret the expression
int result = expression.interpret(context);
System.out.println("Result of (x + 3) - y = " + result);
// Another expression: 7 + 2
Expression simpleExpression = new AddExpression(
new NumberExpression("7"),
new NumberExpression("2")
);
System.out.println("Result of 7 + 2 = " + simpleExpression.interpret(context));
}
}
/*
Result of (x + 3) - y = 8
Result of 7 + 2 = 9
*/
Code language: PHP (php)
Use case and Implementation
Parsing and executing financial scripts or custom transaction rules defined by users.
//CommandPatternDemo.java import java.util.*; // BankAccount class to manage balance class BankAccount { private int balance; public BankAccount(int initialBalance) { this.balance = initialBalance; } public void deposit(int amount) { balance += amount; } public void withdraw(int amount) { if (balance >= amount) { balance -= amount; } else { System.out.println("Insufficient balance for withdrawal of: " + amount); } } public int getBalance() { return balance; } } // Command Interface interface Command { void execute(); void undo(); } // Concrete Command: Transaction class Transaction implements Command { private BankAccount account; private String operation; private int amount; public Transaction(BankAccount account, String operation, int amount) { this.account = account; this.operation = operation; this.amount = amount; } @Override public void execute() { if ("Deposit".equals(operation)) { account.deposit(amount); System.out.println("Deposited: " + amount + ", Balance: " + account.getBalance()); } else if ("Withdraw".equals(operation)) { account.withdraw(amount); System.out.println("Withdrew: " + amount + ", Balance: " + account.getBalance()); } } @Override public void undo() { if ("Deposit".equals(operation)) { account.withdraw(amount); // Undo deposit by withdrawing the amount System.out.println("Undoing deposit of: " + amount + ", Balance: " + account.getBalance()); } else if ("Withdraw".equals(operation)) { account.deposit(amount); // Undo withdraw by depositing the amount back System.out.println("Undoing withdrawal of: " + amount + ", Balance: " + account.getBalance()); } } } // Invoker: TransactionManager class TransactionManager { private List<Command> transactions = new ArrayList<>(); public void addTransaction(Command transaction) { transactions.add(transaction); } public void executeTransactions() { for (Command transaction : transactions) { transaction.execute(); } } public void undoLastTransaction() { if (!transactions.isEmpty()) { Command lastTransaction = transactions.remove(transactions.size() - 1); lastTransaction.undo(); } else { System.out.println("No transactions to undo."); } } } // Client public class CommandPatternDemo { public static void main(String[] args) { BankAccount account = new BankAccount(1000); // Initial balance of 1000 TransactionManager manager = new TransactionManager(); System.out.println("Initial Balance: " + account.getBalance()); // Adding transactions manager.addTransaction(new Transaction(account, "Deposit", 1000)); manager.addTransaction(new Transaction(account, "Withdraw", 500)); // Executing transactions System.out.println("\nExecuting Transactions:"); manager.executeTransactions(); // Balance after transactions System.out.println("\nBalance After Transactions: " + account.getBalance()); // Undoing the last transaction System.out.println("\nUndoing Last Transaction:"); manager.undoLastTransaction(); // Balance after undoing System.out.println("\nBalance After Undo: " + account.getBalance()); } } /* C:\>javac CommandPatternDemo.java C:\>java CommandPatternDemo Initial Balance: 1000 Executing Transactions: Deposited: 1000, Balance: 2000 Withdrew: 500, Balance: 1500 Balance After Transactions: 1500 Undoing Last Transaction: Undoing withdrawal of: 500, Balance: 2000 Balance After Undo: 2000 */
Pros
- Modularity: Each expression type is encapsulated, making it easy to add new expressions (e.g., multiplication, division).
- Extensibility: New grammar rules can be added by creating new expression classes.
- Reusability: The same expression classes can interpret different sentences in the language.
- Clear Structure: Represents the grammar as a class hierarchy, aligning with the language’s syntax.
Cons
- Complexity: Requires a class for each grammar rule, which can lead to a large number of classes for complex grammars.
- Performance: Recursive interpretation of complex expressions can be slow.
- Scalability: Not ideal for very complex languages, where parser generators (e.g., ANTLR, JavaCC) are more efficient.
- Maintenance: Managing many expression classes can be cumbersome for large grammars.
When to Use
- When you need to interpret a simple domain-specific language or grammar.
- When you want to evaluate expressions or sentences defined by a clear set of rules.
- When the grammar is relatively small and doesn’t require a full-fledged parser.
- When you need to provide a way for users to define custom expressions (e.g., rule engines, calculators).
Real-World Example
- SQL Interpreters: Parsing and evaluating SQL queries by breaking them into expressions (e.g., SELECT, WHERE).
- Rule Engines: Evaluating business rules defined as expressions (e.g., “if age > 18 and income > 50000”).
- Mathematical Calculators: Interpreting expressions like 2 + 3 * 4 in a calculator application.
- Configuration Scripts: Interpreting simple scripting languages for configuration files.
The Interpreter Pattern is a behavioral design pattern that provides a way to evaluate language grammar or expressions. It defines a grammatical representation for a language and provides an interpreter to process the sentences in the language.
This pattern is particularly useful when dealing with simple scripting languages, command interpreters, or expression evaluators, where the grammar is relatively small and simple. It promotes flexibility and extensibility by allowing new expressions or rules to be added easily without modifying the existing interpreter structure.