TypeScript Interpreter Design Pattern Example

1. Definition

The Interpreter design pattern provides a way to evaluate language grammar or expressions for particular languages. It mainly involves defining a domain language (i.e., the specific language that needs interpretation) and then interpreting sentences or expressions of the language.

2. Problem Statement

Imagine you're building a software system that requires a feature to evaluate mathematical expressions. Hardcoding each type of expression or using condition-based parsing can become complex, especially if you want to extend it for new types of expressions.

3. Solution

Define a grammar for the language, create an interpreter, and then use the interpreter to interpret expressions in the language. This allows for a more structured approach and easy scalability for new types of expressions.

4. Real-World Use Cases

1. Evaluating mathematical expressions.

2. SQL interpreters in databases.

3. Code compilers for programming languages.

5. Implementation Steps

1. Define an abstract Expression that declares an interpret() method.

2. For every rule in the grammar, define a concrete class implementing the interpret() method.

3. The client then constructs the sentence in the form of an abstract syntax tree and invokes the interpret() method.

6. Implementation in TypeScript

// Step 1: Define the Expression interface
interface Expression {
    interpret(context: string): boolean;
}
// Step 2: Implement concrete Expression classes
class TerminalExpression implements Expression {
    private data: string;
    constructor(data: string) {
        this.data = data;
    }
    interpret(context: string): boolean {
        return context.includes(this.data);
    }
}
class OrExpression implements Expression {
    private expr1: Expression;
    private expr2: Expression;
    constructor(expr1: Expression, expr2: Expression) {
        this.expr1 = expr1;
        this.expr2 = expr2;
    }
    interpret(context: string): boolean {
        return this.expr1.interpret(context) || this.expr2.interpret(context);
    }
}
class AndExpression implements Expression {
    private expr1: Expression;
    private expr2: Expression;
    constructor(expr1: Expression, expr2: Expression) {
        this.expr1 = expr1;
        this.expr2 = expr2;
    }
    interpret(context: string): boolean {
        return this.expr1.interpret(context) && this.expr2.interpret(context);
    }
}
// Client code
const john = new TerminalExpression("John");
const married = new TerminalExpression("Married");
const isMarriedJohn = new AndExpression(john, married);
console.log(`Is John married? ${isMarriedJohn.interpret('Married John')}`);

Output:

Is John married? true

Explanation:

The Interpreter pattern example above interprets whether John is married based on the input string. The TerminalExpression class interprets individual data, while the OrExpression and AndExpression classes allow for combining expressions. By constructing these classes in a specific way, we can interpret complex conditions or expressions.

7. When to use?

Use the Interpreter pattern when:

1. There's a grammar (language) to interpret, and the grammar is simple.

2. Efficiency is not a primary concern.

3. The parsing or interpreting can be broken down into smaller, simpler sub-tasks or expressions.


Comments