Skip to content

๐Ÿ”  Interpreter Pattern: Smart Home Rule Engine

๐Ÿ“ Overview

The Interpreter Pattern defines a representation for a grammar and provides an evaluator that interprets sentences in that language. It is best used for building rule engines, query languages, or domain-specific languages (DSLs) where logic needs to be defined dynamically at runtime.

Core Concepts

  • Abstract Syntax Tree (AST): Sentences are parsed into a tree where nodes represent grammar rules.
  • Recursive Evaluation: Each node in the tree knows how to interpret itself by calling interpret() on its children.
  • Context: A global state object passed around during interpretation to look up variable values.

๐Ÿญ The Engineering Story & Problem

๐Ÿ˜ก The Villain (The Problem)

Imagine a "Hardcoded Rule System" for a smart home. Users want to define automation rules like "IF temp > 30 AND motion = detected THEN turn_on_ac". In the bad version, you write a parser that spits out a massive nested if-else block or, worse, uses Python's dangerous eval() function on raw strings. Adding a new operator like "XOR" or "NOT" requires hacking the core parsing logic. The business logic is buried in the parser, making it rigid and unsafe.

๐Ÿฆธ The Hero (The Solution)

The Interpreter Pattern treats the rule as a "Sentence" in a language. We break the sentence down into small grammatical parts (Tokens).
- Terminal Expressions: The leaves of the tree (e.g., 30, temp).
- Non-Terminal Expressions: The branches (e.g., AndExpression, GreaterThanExpression).
We build a tree: And(GreaterThan(temp, 30), Equals(motion, detected)). To run it, we just call root.interpret(context). The And node calls interpret on its two children and combines the result.

๐Ÿ“œ Requirements & Constraints

  1. (Functional): Support boolean logic (AND, OR) and comparison operators (>, <).
  2. (Technical): The system must parse string rules into an executable object tree.
  3. (Technical): Context (sensor data) is provided at runtime, separate from the rule definition.

๐Ÿ—๏ธ Structure & Blueprint

Class Diagram

classDiagram
    direction TB
    class Expression {
        <<interface>>
        +interpret(context: Dictionary) boolean
    }
    class TerminalExpression {
        -variable: str
        +interpret(context)
    }
    class AndExpression {
        -left: Expression
        -right: Expression
        +interpret(context)
    }
    class OrExpression {
        -left: Expression
        -right: Expression
        +interpret(context)
    }

    Expression <|.. TerminalExpression
    Expression <|.. AndExpression
    Expression <|.. OrExpression
    AndExpression o-- Expression

Runtime Context (Sequence)

sequenceDiagram
    participant Client
    participant AndExpr
    participant GreaterThanExpr
    participant Context

    Client->>AndExpr: interpret(context)
    AndExpr->>GreaterThanExpr: interpret(context)
    GreaterThanExpr->>Context: get("temperature")
    Context-->>GreaterThanExpr: 35
    GreaterThanExpr-->>AndExpr: true

    AndExpr->>OtherExpr: interpret(context)
    OtherExpr-->>AndExpr: true

    AndExpr-->>Client: true

๐Ÿ’ป Implementation & Code

๐Ÿง  SOLID Principles Applied

  • Open/Closed: You can add a XorExpression class without modifying the existing AndExpression or parsing logic.
  • Single Responsibility: Each class handles the logic for exactly one grammar rule.

๐Ÿ The Code

The Villain's Code (Without Pattern)
def check_rules(rule_string, data):
    # ๐Ÿ˜ก The Dangerous/Rigid Way
    # 1. Security risk (eval)
    # 2. Or massive hardcoded parsing logic
    if "AND" in rule_string:
        parts = rule_string.split("AND")
        # ... parsing hell ...

    # DANGEROUS:
    # return eval(rule_string, {}, data) 
The Hero's Code (With Pattern)
from abc import ABC, abstractmethod

class Context:
    def __init__(self, data: dict) -> None:
        self.data: dict = data # e.g., {"TEMP": 32, "HUMIDITY": 75}

    def get_value(self, name: str) -> int:
        return self.data.get(name, 0)


class Expression(ABC):
    @abstractmethod
    def interpret(self, context: Context) -> bool:
        pass

# Leaf Expressions
class NumberExpression(Expression):
    def __init__(self, value: int) -> None:
        self.value: int = value

    def interpret(self, context: Context) -> int:
        return self.value


class VariableExpression(Expression):
    def __init__(self, name: str) -> None:
        self.name: str = name

    def interpret(self, context: Context) -> int:
        return context.get_value(name=self.name)


# Non-Leaf Expressions
class AndExpression(Expression):
    def __init__(self, left: Expression, right: Expression) -> None:
        self.left: Expression = left
        self.right: Expression = right

    def interpret(self, context: Context) -> bool:
        return self.left.interpret(context=context) and self.right.interpret(context=context)


class OrExpression(Expression):
    def __init__(self, left: Expression, right: Expression) -> None:
        self.left: Expression = left
        self.right: Expression = right

    def interpret(self, context: Context) -> bool:
        return self.left.interpret(context=context) or self.right.interpret(context=context)


class NotExpression(Expression):
    def __init__(self, expression: Expression) -> None:
        self.expression: Expression = expression

    def interpret(self, context: Context) -> bool:
        return not self.expression.interpret(context=context)


class GreaterThanExpression(Expression):
    def __init__(self, left: Expression, right: Expression) -> None:
        self.left: Expression = left
        self.right: Expression = right

    def interpret(self, context: Context) -> bool:
        return self.left.interpret(context=context) > self.right.interpret(context=context)


class EqualExpression(Expression):
    def __init__(self, left: Expression, right: Expression) -> None:
        self.left: Expression = left
        self.right: Expression = right

    def interpret(self, context: Context) -> bool:
        return self.left.interpret(context=context) == self.right.interpret(context=context)


class LessThanExpression(Expression):
    def __init__(self, left: Expression, right: Expression) -> None:
        self.left: Expression = left
        self.right: Expression = right

    def interpret(self, context: Context) -> bool:
        return self.left.interpret(context=context) < self.right.interpret(context=context)


class Parser:
    def __init__(self, input: str) -> None:
        self.input: str = input

    def _parse(self) -> Expression:
        expressions = self.input.split()

        def comparison():
            left_name = expressions.pop(0)
            operator = expressions.pop(0)
            right_val = expressions.pop(0)

            left_expression = VariableExpression(name=left_name)
            right_expression = NumberExpression(value=int(right_val))

            if operator == ">": return GreaterThanExpression(left=left_expression, right=right_expression)
            if operator == "==": return EqualExpression(left=left_expression, right=right_expression)
            return LessThanExpression(left=left_expression, right=right_expression)

        logical_operator = expressions[0]
        if logical_operator == "NOT":
            expressions.pop(0)
            final_expression = NotExpression(expression=comparison())
        else:
            final_expression = comparison()

        while expressions:
            logical_operator = expressions.pop(0)
            right_comparision = comparison()

            if logical_operator == "AND":
                final_expression = AndExpression(left=final_expression, right=right_comparision)
            elif logical_operator == "OR":
                final_expression = OrExpression(left=final_expression, right=right_comparision)

        return final_expression

# Setup
context = Context({"TEMP": 32, "HUMIDITY": 75})
parser = Parser("TEMP > 30 AND HUMIDITY > 70")

# Execute
tree = parser._parse()
result = tree.interpret(context)

print(f"Is the rule triggered? {result}") # Should be True

โš–๏ธ Trade-offs & Testing

Pros (Why it works) Cons (The Twist / Pitfalls)
Extensibility: Easy to add new grammar rules. Complexity: A class for every grammar rule.
Safety: Sandboxed execution (unlike eval). Performance: Recursive calls can be slow for huge trees.
Modularity: Logic is broken down into tiny units. Hard to Maintain: If the grammar gets too big, the number of classes explodes.

๐Ÿงช Testing Strategy

  1. Unit Test Expressions: Test AndExpression with hardcoded true/false children.
  2. Integration Test: Parse a full string "temp > 10" and verify interpret({'temp': 20}) returns True.

๐ŸŽค Interview Toolkit

  • Interview Signal: mastery of recursion, trees, and language design.
  • When to Use:
    • "Design a rule engine..."
    • "Implement a custom query language..."
    • "Evaluate complex logical expressions..."
  • Scalability Probe: "What if the expression tree is 10,000 nodes deep?" (Answer: Recursion depth limit. Use the Visitor pattern or an iterative stack-based approach.)
  • Design Alternatives:
    • Composite: The Interpreter structure is a Composite.
    • Visitor: Used to separate the operation (interpret, pretty-print) from the structure.
  • Composite โ€” The structure of the AST is a Composite.
  • Visitor โ€” Can be used to traverse the tree and perform operations (like validation or printing) without changing the classes.
  • Flyweight โ€” Terminal nodes (like the string "temperature") can be shared.