Interpreter Pattern #

Sistem monitoring perlu menerima filter query dari pengguna seperti status == "active" AND age > 25 AND city IN ["Jakarta", "Bandung"]. Query ini bukan string statis — ia bisa berbeda setiap saat, bisa disimpan di database, bisa di-compose secara dinamis. Kamu tidak bisa hardcode setiap kombinasi yang mungkin. Interpreter Pattern memberikan solusi: definisikan grammar untuk bahasa filter, bangun parser yang mengubah string menjadi tree of expression objects, dan evaluasikan tree tersebut terhadap data yang ada. Setiap operator (==, >, AND, IN) menjadi class yang tahu cara mengevaluasi dirinya sendiri. Menambah operator baru berarti menambah satu class baru — tanpa mengubah class operator yang sudah ada.

Apa itu Interpreter Pattern? #

Interpreter Pattern adalah behavioral design pattern yang mendefinisikan representasi grammar untuk suatu bahasa, dan menyediakan interpreter untuk menginterpretasikan kalimat dalam bahasa tersebut. Setiap aturan grammar direpresentasikan sebagai class, dan kalimat dalam bahasa tersebut direpresentasikan sebagai Abstract Syntax Tree (AST) dari objek-objek tersebut.

Tiga komponen inti Interpreter Pattern:

  • Abstract Expression — interface yang mendefinisikan operasi Interpret
  • Terminal Expression — expression leaf yang merepresentasikan nilai atau variabel; tidak mengandung expression lain
  • Non-terminal Expression — expression yang mengandung satu atau lebih expression lain; merepresentasikan aturan grammar yang rekursif

Interpreter Pattern adalah fondasi dari: query language, template engine, rule engine, kalkulator ekspresi, konfigurasi berbasis DSL, dan banyak sistem parsing lainnya.

flowchart TD
    subgraph "Input: Query String"
        Q['"status == active AND age > 25"']
    end

    subgraph "Parse → AST"
        AND[AndExpression]
        EQ[EqualsExpression\nstatus = active]
        GT[GreaterThanExpression\nage > 25]
        AND --> EQ
        AND --> GT
    end

    subgraph "Evaluate dengan Context"
        CTX["Context: {status: active, age: 30}"]
        RES["Result: true"]
    end

    Q -->|parse| AND
    AND -->|interpret ctx| RES
    CTX --> RES

Grammar dan Expression Tree #

Sebelum menulis kode, penting memahami grammar yang akan diimplementasikan. Grammar mendefinisikan aturan bahasa secara formal.

Grammar untuk filter query sederhana:

expression     = or_expression
or_expression  = and_expression ("OR" and_expression)*
and_expression = comparison ("AND" comparison)*
comparison     = identifier operator value
               | "NOT" comparison
               | "(" expression ")"
operator       = "==" | "!=" | ">" | ">=" | "<" | "<=" | "IN" | "CONTAINS"
value          = string | number | boolean | array
identifier     = [a-zA-Z_][a-zA-Z0-9_.]*

Setiap baris grammar menjadi satu atau beberapa Expression class:

Grammar Rule Expression Class
or_expression OrExpression
and_expression AndExpression
NOT comparison NotExpression
identifier == value EqualsExpression
identifier > value GreaterThanExpression
identifier IN array InExpression
identifier FieldExpression
value (literal) LiteralExpression

Implementasi Lengkap: Filter Query Engine #

Context dan Abstract Expression #

package filter

import (
    "fmt"
    "strings"
)

// Context menyimpan data yang akan dievaluasi terhadap expression.
// Ini adalah "environment" saat interpretasi berjalan.
type Context map[string]interface{}

// Get mengambil nilai field dari context, mendukung dot notation (user.age).
func (c Context) Get(field string) (interface{}, bool) {
    parts := strings.SplitN(field, ".", 2)
    val, ok := c[parts[0]]
    if !ok || len(parts) == 1 {
        return val, ok
    }
    // Navigasi nested field
    if nested, ok := val.(map[string]interface{}); ok {
        return Context(nested).Get(parts[1])
    }
    return nil, false
}

// Expression adalah Abstract Expression — interface untuk semua expression.
type Expression interface {
    // Interpret mengevaluasi expression dalam context yang diberikan.
    Interpret(ctx Context) (bool, error)

    // String mengembalikan representasi string expression untuk debugging.
    String() string
}

Terminal Expressions #

package filter

import (
    "fmt"
    "reflect"
    "strconv"
    "strings"
)

// LiteralExpression merepresentasikan nilai konstan (string, number, bool).
type LiteralExpression struct {
    Value interface{}
}

func (e *LiteralExpression) Interpret(ctx Context) (bool, error) {
    // Literal tidak bisa langsung di-interpret sebagai bool
    // Digunakan sebagai operand dalam comparison expression
    return false, fmt.Errorf("literal expression cannot be interpreted directly")
}

func (e *LiteralExpression) String() string {
    return fmt.Sprintf("%v", e.Value)
}

// getValue mengembalikan nilai literal untuk digunakan oleh parent expression.
func (e *LiteralExpression) getValue() interface{} { return e.Value }


// FieldExpression mengakses nilai field dari context.
type FieldExpression struct {
    FieldName string
}

func (e *FieldExpression) Interpret(ctx Context) (bool, error) {
    val, ok := ctx.Get(e.FieldName)
    if !ok {
        return false, nil
    }
    if b, ok := val.(bool); ok {
        return b, nil
    }
    return false, fmt.Errorf("field %s is not boolean", e.FieldName)
}

func (e *FieldExpression) String() string { return e.FieldName }

func (e *FieldExpression) getValue(ctx Context) interface{} {
    val, _ := ctx.Get(e.FieldName)
    return val
}


// EqualsExpression mengevaluasi field == value.
type EqualsExpression struct {
    Field string
    Value interface{}
}

func NewEqualsExpression(field string, value interface{}) Expression {
    return &EqualsExpression{Field: field, Value: value}
}

func (e *EqualsExpression) Interpret(ctx Context) (bool, error) {
    fieldVal, ok := ctx.Get(e.Field)
    if !ok {
        return e.Value == nil, nil
    }
    return reflect.DeepEqual(fieldVal, e.Value), nil
}

func (e *EqualsExpression) String() string {
    return fmt.Sprintf("%s == %v", e.Field, e.Value)
}


// NotEqualsExpression mengevaluasi field != value.
type NotEqualsExpression struct {
    Field string
    Value interface{}
}

func NewNotEqualsExpression(field string, value interface{}) Expression {
    return &NotEqualsExpression{Field: field, Value: value}
}

func (e *NotEqualsExpression) Interpret(ctx Context) (bool, error) {
    eq := &EqualsExpression{Field: e.Field, Value: e.Value}
    result, err := eq.Interpret(ctx)
    return !result, err
}

func (e *NotEqualsExpression) String() string {
    return fmt.Sprintf("%s != %v", e.Field, e.Value)
}


// GreaterThanExpression mengevaluasi field > value.
type GreaterThanExpression struct {
    Field string
    Value float64
}

func NewGreaterThanExpression(field string, value float64) Expression {
    return &GreaterThanExpression{Field: field, Value: value}
}

func (e *GreaterThanExpression) Interpret(ctx Context) (bool, error) {
    fieldVal, ok := ctx.Get(e.Field)
    if !ok {
        return false, nil
    }
    num, err := toNumber(fieldVal)
    if err != nil {
        return false, fmt.Errorf("field %s: %w", e.Field, err)
    }
    return num > e.Value, nil
}

func (e *GreaterThanExpression) String() string {
    return fmt.Sprintf("%s > %v", e.Field, e.Value)
}


// LessThanExpression mengevaluasi field < value.
type LessThanExpression struct {
    Field string
    Value float64
}

func NewLessThanExpression(field string, value float64) Expression {
    return &LessThanExpression{Field: field, Value: value}
}

func (e *LessThanExpression) Interpret(ctx Context) (bool, error) {
    fieldVal, ok := ctx.Get(e.Field)
    if !ok {
        return false, nil
    }
    num, err := toNumber(fieldVal)
    if err != nil {
        return false, fmt.Errorf("field %s: %w", e.Field, err)
    }
    return num < e.Value, nil
}

func (e *LessThanExpression) String() string {
    return fmt.Sprintf("%s < %v", e.Field, e.Value)
}


// InExpression mengevaluasi field IN [value1, value2, ...].
type InExpression struct {
    Field  string
    Values []interface{}
}

func NewInExpression(field string, values []interface{}) Expression {
    return &InExpression{Field: field, Values: values}
}

func (e *InExpression) Interpret(ctx Context) (bool, error) {
    fieldVal, ok := ctx.Get(e.Field)
    if !ok {
        return false, nil
    }
    for _, v := range e.Values {
        if reflect.DeepEqual(fieldVal, v) {
            return true, nil
        }
    }
    return false, nil
}

func (e *InExpression) String() string {
    vals := make([]string, len(e.Values))
    for i, v := range e.Values {
        vals[i] = fmt.Sprintf("%v", v)
    }
    return fmt.Sprintf("%s IN [%s]", e.Field, strings.Join(vals, ", "))
}


// ContainsExpression mengevaluasi apakah string field mengandung substring.
type ContainsExpression struct {
    Field     string
    Substring string
}

func NewContainsExpression(field, substring string) Expression {
    return &ContainsExpression{Field: field, Substring: substring}
}

func (e *ContainsExpression) Interpret(ctx Context) (bool, error) {
    fieldVal, ok := ctx.Get(e.Field)
    if !ok {
        return false, nil
    }
    str, ok := fieldVal.(string)
    if !ok {
        return false, fmt.Errorf("field %s is not a string", e.Field)
    }
    return strings.Contains(str, e.Substring), nil
}

func (e *ContainsExpression) String() string {
    return fmt.Sprintf("%s CONTAINS %q", e.Field, e.Substring)
}

func toNumber(v interface{}) (float64, error) {
    switch val := v.(type) {
    case float64:
        return val, nil
    case float32:
        return float64(val), nil
    case int:
        return float64(val), nil
    case int64:
        return float64(val), nil
    case string:
        n, err := strconv.ParseFloat(val, 64)
        if err != nil {
            return 0, fmt.Errorf("cannot convert %q to number", val)
        }
        return n, nil
    }
    return 0, fmt.Errorf("cannot convert %T to number", v)
}

Non-terminal Expressions #

package filter

import "fmt"

// AndExpression mengevaluasi left AND right.
// Non-terminal: mengandung dua expression lain.
type AndExpression struct {
    Left  Expression
    Right Expression
}

func NewAndExpression(left, right Expression) Expression {
    return &AndExpression{Left: left, Right: right}
}

func (e *AndExpression) Interpret(ctx Context) (bool, error) {
    // Short-circuit evaluation: jika kiri false, tidak perlu evaluasi kanan
    left, err := e.Left.Interpret(ctx)
    if err != nil {
        return false, fmt.Errorf("AND left: %w", err)
    }
    if !left {
        return false, nil // short-circuit
    }

    right, err := e.Right.Interpret(ctx)
    if err != nil {
        return false, fmt.Errorf("AND right: %w", err)
    }
    return right, nil
}

func (e *AndExpression) String() string {
    return fmt.Sprintf("(%s AND %s)", e.Left.String(), e.Right.String())
}


// OrExpression mengevaluasi left OR right.
type OrExpression struct {
    Left  Expression
    Right Expression
}

func NewOrExpression(left, right Expression) Expression {
    return &OrExpression{Left: left, Right: right}
}

func (e *OrExpression) Interpret(ctx Context) (bool, error) {
    // Short-circuit: jika kiri true, tidak perlu evaluasi kanan
    left, err := e.Left.Interpret(ctx)
    if err != nil {
        return false, fmt.Errorf("OR left: %w", err)
    }
    if left {
        return true, nil // short-circuit
    }

    right, err := e.Right.Interpret(ctx)
    if err != nil {
        return false, fmt.Errorf("OR right: %w", err)
    }
    return right, nil
}

func (e *OrExpression) String() string {
    return fmt.Sprintf("(%s OR %s)", e.Left.String(), e.Right.String())
}


// NotExpression mengevaluasi NOT expression.
type NotExpression struct {
    Expr Expression
}

func NewNotExpression(expr Expression) Expression {
    return &NotExpression{Expr: expr}
}

func (e *NotExpression) Interpret(ctx Context) (bool, error) {
    result, err := e.Expr.Interpret(ctx)
    if err != nil {
        return false, fmt.Errorf("NOT: %w", err)
    }
    return !result, nil
}

func (e *NotExpression) String() string {
    return fmt.Sprintf("(NOT %s)", e.Expr.String())
}

Parser: Mengubah String menjadi Expression Tree #

package filter

import (
    "fmt"
    "strconv"
    "strings"
    "unicode"
)

// Token merepresentasikan satu unit leksikal dalam query string.
type Token struct {
    Type  TokenType
    Value string
}

type TokenType int

const (
    TOKEN_IDENTIFIER TokenType = iota
    TOKEN_STRING
    TOKEN_NUMBER
    TOKEN_BOOL
    TOKEN_AND
    TOKEN_OR
    TOKEN_NOT
    TOKEN_IN
    TOKEN_CONTAINS
    TOKEN_EQ
    TOKEN_NEQ
    TOKEN_GT
    TOKEN_GTE
    TOKEN_LT
    TOKEN_LTE
    TOKEN_LPAREN
    TOKEN_RPAREN
    TOKEN_LBRACKET
    TOKEN_RBRACKET
    TOKEN_COMMA
    TOKEN_EOF
)

// Lexer memecah query string menjadi token-token.
type Lexer struct {
    input []rune
    pos   int
}

func NewLexer(input string) *Lexer {
    return &Lexer{input: []rune(input)}
}

func (l *Lexer) skipWhitespace() {
    for l.pos < len(l.input) && unicode.IsSpace(l.input[l.pos]) {
        l.pos++
    }
}

func (l *Lexer) NextToken() Token {
    l.skipWhitespace()
    if l.pos >= len(l.input) {
        return Token{Type: TOKEN_EOF}
    }

    ch := l.input[l.pos]

    // String literal
    if ch == '"' || ch == '\'' {
        return l.readString(ch)
    }

    // Number
    if unicode.IsDigit(ch) || (ch == '-' && l.pos+1 < len(l.input) && unicode.IsDigit(l.input[l.pos+1])) {
        return l.readNumber()
    }

    // Operators
    switch ch {
    case '(':
        l.pos++
        return Token{Type: TOKEN_LPAREN, Value: "("}
    case ')':
        l.pos++
        return Token{Type: TOKEN_RPAREN, Value: ")"}
    case '[':
        l.pos++
        return Token{Type: TOKEN_LBRACKET, Value: "["}
    case ']':
        l.pos++
        return Token{Type: TOKEN_RBRACKET, Value: "]"}
    case ',':
        l.pos++
        return Token{Type: TOKEN_COMMA, Value: ","}
    case '=':
        if l.pos+1 < len(l.input) && l.input[l.pos+1] == '=' {
            l.pos += 2
            return Token{Type: TOKEN_EQ, Value: "=="}
        }
    case '!':
        if l.pos+1 < len(l.input) && l.input[l.pos+1] == '=' {
            l.pos += 2
            return Token{Type: TOKEN_NEQ, Value: "!="}
        }
    case '>':
        if l.pos+1 < len(l.input) && l.input[l.pos+1] == '=' {
            l.pos += 2
            return Token{Type: TOKEN_GTE, Value: ">="}
        }
        l.pos++
        return Token{Type: TOKEN_GT, Value: ">"}
    case '<':
        if l.pos+1 < len(l.input) && l.input[l.pos+1] == '=' {
            l.pos += 2
            return Token{Type: TOKEN_LTE, Value: "<="}
        }
        l.pos++
        return Token{Type: TOKEN_LT, Value: "<"}
    }

    // Identifier atau keyword
    if unicode.IsLetter(ch) || ch == '_' {
        return l.readIdentifierOrKeyword()
    }

    l.pos++
    return Token{Type: TOKEN_EOF}
}

func (l *Lexer) readString(quote rune) Token {
    l.pos++ // skip opening quote
    start := l.pos
    for l.pos < len(l.input) && l.input[l.pos] != quote {
        l.pos++
    }
    value := string(l.input[start:l.pos])
    if l.pos < len(l.input) {
        l.pos++ // skip closing quote
    }
    return Token{Type: TOKEN_STRING, Value: value}
}

func (l *Lexer) readNumber() Token {
    start := l.pos
    if l.input[l.pos] == '-' {
        l.pos++
    }
    for l.pos < len(l.input) && (unicode.IsDigit(l.input[l.pos]) || l.input[l.pos] == '.') {
        l.pos++
    }
    return Token{Type: TOKEN_NUMBER, Value: string(l.input[start:l.pos])}
}

func (l *Lexer) readIdentifierOrKeyword() Token {
    start := l.pos
    for l.pos < len(l.input) && (unicode.IsLetter(l.input[l.pos]) || unicode.IsDigit(l.input[l.pos]) || l.input[l.pos] == '_' || l.input[l.pos] == '.') {
        l.pos++
    }
    value := string(l.input[start:l.pos])
    upper := strings.ToUpper(value)

    switch upper {
    case "AND":
        return Token{Type: TOKEN_AND, Value: "AND"}
    case "OR":
        return Token{Type: TOKEN_OR, Value: "OR"}
    case "NOT":
        return Token{Type: TOKEN_NOT, Value: "NOT"}
    case "IN":
        return Token{Type: TOKEN_IN, Value: "IN"}
    case "CONTAINS":
        return Token{Type: TOKEN_CONTAINS, Value: "CONTAINS"}
    case "TRUE":
        return Token{Type: TOKEN_BOOL, Value: "true"}
    case "FALSE":
        return Token{Type: TOKEN_BOOL, Value: "false"}
    default:
        return Token{Type: TOKEN_IDENTIFIER, Value: value}
    }
}

// Parser mengubah token stream menjadi Expression tree menggunakan recursive descent.
type Parser struct {
    lexer   *Lexer
    current Token
}

func NewParser(query string) *Parser {
    p := &Parser{lexer: NewLexer(query)}
    p.current = p.lexer.NextToken() // load token pertama
    return p
}

func (p *Parser) consume() Token {
    tok := p.current
    p.current = p.lexer.NextToken()
    return tok
}

// Parse menghasilkan Expression tree dari query.
func (p *Parser) Parse() (Expression, error) {
    return p.parseOr()
}

func (p *Parser) parseOr() (Expression, error) {
    left, err := p.parseAnd()
    if err != nil {
        return nil, err
    }

    for p.current.Type == TOKEN_OR {
        p.consume()
        right, err := p.parseAnd()
        if err != nil {
            return nil, err
        }
        left = NewOrExpression(left, right)
    }
    return left, nil
}

func (p *Parser) parseAnd() (Expression, error) {
    left, err := p.parseComparison()
    if err != nil {
        return nil, err
    }

    for p.current.Type == TOKEN_AND {
        p.consume()
        right, err := p.parseComparison()
        if err != nil {
            return nil, err
        }
        left = NewAndExpression(left, right)
    }
    return left, nil
}

func (p *Parser) parseComparison() (Expression, error) {
    // NOT expression
    if p.current.Type == TOKEN_NOT {
        p.consume()
        expr, err := p.parseComparison()
        if err != nil {
            return nil, err
        }
        return NewNotExpression(expr), nil
    }

    // Parenthesized expression
    if p.current.Type == TOKEN_LPAREN {
        p.consume()
        expr, err := p.parseOr()
        if err != nil {
            return nil, err
        }
        if p.current.Type != TOKEN_RPAREN {
            return nil, fmt.Errorf("expected ), got %q", p.current.Value)
        }
        p.consume()
        return expr, nil
    }

    // identifier operator value
    if p.current.Type != TOKEN_IDENTIFIER {
        return nil, fmt.Errorf("expected identifier, got %q", p.current.Value)
    }
    field := p.consume().Value

    switch p.current.Type {
    case TOKEN_EQ:
        p.consume()
        value, err := p.parseValue()
        if err != nil {
            return nil, err
        }
        return NewEqualsExpression(field, value), nil

    case TOKEN_NEQ:
        p.consume()
        value, err := p.parseValue()
        if err != nil {
            return nil, err
        }
        return NewNotEqualsExpression(field, value), nil

    case TOKEN_GT:
        p.consume()
        num, err := p.parseNumber()
        if err != nil {
            return nil, err
        }
        return NewGreaterThanExpression(field, num), nil

    case TOKEN_LT:
        p.consume()
        num, err := p.parseNumber()
        if err != nil {
            return nil, err
        }
        return NewLessThanExpression(field, num), nil

    case TOKEN_IN:
        p.consume()
        values, err := p.parseArray()
        if err != nil {
            return nil, err
        }
        return NewInExpression(field, values), nil

    case TOKEN_CONTAINS:
        p.consume()
        if p.current.Type != TOKEN_STRING {
            return nil, fmt.Errorf("CONTAINS requires string value")
        }
        substring := p.consume().Value
        return NewContainsExpression(field, substring), nil
    }

    return nil, fmt.Errorf("unexpected token %q after identifier %q", p.current.Value, field)
}

func (p *Parser) parseValue() (interface{}, error) {
    switch p.current.Type {
    case TOKEN_STRING:
        return p.consume().Value, nil
    case TOKEN_NUMBER:
        num, err := strconv.ParseFloat(p.consume().Value, 64)
        if err != nil {
            return nil, fmt.Errorf("invalid number: %w", err)
        }
        return num, nil
    case TOKEN_BOOL:
        val := p.consume().Value == "true"
        return val, nil
    }
    return nil, fmt.Errorf("expected value, got %q", p.current.Value)
}

func (p *Parser) parseNumber() (float64, error) {
    if p.current.Type != TOKEN_NUMBER {
        return 0, fmt.Errorf("expected number, got %q", p.current.Value)
    }
    return strconv.ParseFloat(p.consume().Value, 64)
}

func (p *Parser) parseArray() ([]interface{}, error) {
    if p.current.Type != TOKEN_LBRACKET {
        return nil, fmt.Errorf("expected [, got %q", p.current.Value)
    }
    p.consume()

    var values []interface{}
    for p.current.Type != TOKEN_RBRACKET && p.current.Type != TOKEN_EOF {
        val, err := p.parseValue()
        if err != nil {
            return nil, err
        }
        values = append(values, val)

        if p.current.Type == TOKEN_COMMA {
            p.consume()
        }
    }

    if p.current.Type != TOKEN_RBRACKET {
        return nil, fmt.Errorf("expected ], got EOF")
    }
    p.consume()
    return values, nil
}

// ParseQuery adalah fungsi helper untuk parse dan return Expression.
func ParseQuery(query string) (Expression, error) {
    parser := NewParser(query)
    return parser.Parse()
}

Query Engine: Menyatukan Semua #

package filter

// QueryEngine mengevaluasi query terhadap kumpulan data.
type QueryEngine struct {
    cache map[string]Expression // cache parsed expression
}

func NewQueryEngine() *QueryEngine {
    return &QueryEngine{cache: make(map[string]Expression)}
}

// Filter mengevaluasi query terhadap setiap item dalam slice.
func (e *QueryEngine) Filter(items []Context, query string) ([]Context, error) {
    // Parse query (dengan caching)
    expr, ok := e.cache[query]
    if !ok {
        var err error
        expr, err = ParseQuery(query)
        if err != nil {
            return nil, fmt.Errorf("invalid query: %w", err)
        }
        e.cache[query] = expr
    }

    var results []Context
    for _, item := range items {
        match, err := expr.Interpret(item)
        if err != nil {
            return nil, fmt.Errorf("evaluation error: %w", err)
        }
        if match {
            results = append(results, item)
        }
    }
    return results, nil
}

// Demonstrasi penggunaan
func main() {
    engine := NewQueryEngine()

    users := []Context{
        {"name": "Alice", "age": float64(28), "city": "Jakarta", "status": "active"},
        {"name": "Bob",   "age": float64(35), "city": "Surabaya", "status": "inactive"},
        {"name": "Citra", "age": float64(22), "city": "Bandung",  "status": "active"},
        {"name": "Dani",  "age": float64(45), "city": "Jakarta",  "status": "active"},
        {"name": "Eka",   "age": float64(17), "city": "Medan",    "status": "active"},
    }

    queries := []string{
        `status == "active" AND age > 25`,
        `city IN ["Jakarta", "Bandung"]`,
        `status == "active" AND age > 20 AND NOT city == "Medan"`,
        `name CONTAINS "a"`,
    }

    for _, q := range queries {
        results, err := engine.Filter(users, q)
        if err != nil {
            fmt.Printf("Error: %v\n", err)
            continue
        }
        names := make([]string, len(results))
        for i, r := range results {
            names[i] = r["name"].(string)
        }
        fmt.Printf("Query: %s\nResult: %v\n\n", q, names)
    }
}

Output:

Query: status == "active" AND age > 25
Result: [Alice Dani]

Query: city IN ["Jakarta", "Bandung"]
Result: [Alice Citra Dani]

Query: status == "active" AND age > 20 AND NOT city == "Medan"
Result: [Alice Citra Dani]

Query: name CONTAINS "a"
Result: [Alice Citra Dani Eka]

Testing Interpreter Pattern #

func TestEqualsExpression(t *testing.T) {
    expr := NewEqualsExpression("status", "active")
    ctx := Context{"status": "active"}
    result, err := expr.Interpret(ctx)
    if err != nil || !result {
        t.Errorf("expected true, got %v (err: %v)", result, err)
    }

    ctx2 := Context{"status": "inactive"}
    result2, _ := expr.Interpret(ctx2)
    if result2 {
        t.Error("expected false for non-matching value")
    }
}

func TestAndExpression_ShortCircuit(t *testing.T) {
    called := false
    // Expression yang mencatat apakah dipanggil
    rightExpr := &EqualsExpression{Field: "trigger", Value: "yes"}
    andExpr := NewAndExpression(
        NewEqualsExpression("status", "inactive"), // false — short circuit
        rightExpr,
    )

    ctx := Context{"status": "active", "trigger": "yes"}
    result, _ := andExpr.Interpret(ctx)
    if result {
        t.Error("AND with false left should be false")
    }
    _ = called // dalam implementasi nyata, verifikasi rightExpr tidak di-evaluate
}

func TestInExpression(t *testing.T) {
    expr := NewInExpression("city", []interface{}{"Jakarta", "Bandung"})

    tests := []struct {
        city     string
        expected bool
    }{
        {"Jakarta", true},
        {"Bandung", true},
        {"Surabaya", false},
    }

    for _, tt := range tests {
        ctx := Context{"city": tt.city}
        result, err := expr.Interpret(ctx)
        if err != nil {
            t.Fatalf("unexpected error: %v", err)
        }
        if result != tt.expected {
            t.Errorf("city=%s: expected %v, got %v", tt.city, tt.expected, result)
        }
    }
}

func TestParser_ComplexQuery(t *testing.T) {
    query := `status == "active" AND age > 25`
    expr, err := ParseQuery(query)
    if err != nil {
        t.Fatalf("parse failed: %v", err)
    }

    activeOld := Context{"status": "active", "age": float64(30)}
    result, _ := expr.Interpret(activeOld)
    if !result {
        t.Error("expected true for active user age 30")
    }

    activeYoung := Context{"status": "active", "age": float64(20)}
    result, _ = expr.Interpret(activeYoung)
    if result {
        t.Error("expected false for active user age 20")
    }

    inactiveOld := Context{"status": "inactive", "age": float64(30)}
    result, _ = expr.Interpret(inactiveOld)
    if result {
        t.Error("expected false for inactive user")
    }
}

func TestParser_NotExpression(t *testing.T) {
    expr, err := ParseQuery(`NOT status == "inactive"`)
    if err != nil {
        t.Fatalf("parse failed: %v", err)
    }

    ctx := Context{"status": "active"}
    result, _ := expr.Interpret(ctx)
    if !result {
        t.Error("NOT inactive should be true for active status")
    }
}

func TestParser_OrExpression(t *testing.T) {
    expr, err := ParseQuery(`city == "Jakarta" OR city == "Bandung"`)
    if err != nil {
        t.Fatalf("parse failed: %v", err)
    }

    tests := []struct {
        city     string
        expected bool
    }{
        {"Jakarta", true},
        {"Bandung", true},
        {"Surabaya", false},
    }

    for _, tt := range tests {
        result, _ := expr.Interpret(Context{"city": tt.city})
        if result != tt.expected {
            t.Errorf("city=%s: expected %v, got %v", tt.city, tt.expected, result)
        }
    }
}

func TestQueryEngine_CachesExpression(t *testing.T) {
    engine := NewQueryEngine()
    query := `status == "active"`
    items := []Context{
        {"status": "active"},
        {"status": "inactive"},
    }

    results1, _ := engine.Filter(items, query)
    results2, _ := engine.Filter(items, query) // harus gunakan cache

    if len(results1) != 1 || len(results2) != 1 {
        t.Errorf("expected 1 result, got %d and %d", len(results1), len(results2))
    }
}

Kombinasi Interpreter dengan Composite dan Visitor #

Interpreter Pattern secara natural berkolaborasi dengan dua pattern lain:

flowchart TD
    subgraph "Composite — Struktur Tree"
        AND2[AndExpression\nComposite]
        EQ2[EqualsExpression\nLeaf]
        GT2[GreaterThanExpression\nLeaf]
        AND2 --> EQ2
        AND2 --> GT2
    end

    subgraph "Interpreter — Evaluasi"
        I["Interpret(ctx)\ndipanggil rekursif\nseperti Composite.GetSize()"]
    end

    subgraph "Visitor — Analisis"
        OPT[OptimizeVisitor\n'constant folding']
        STR[StringifyVisitor\n'debug print']
    end

    AND2 -->|rekursif| I
    AND2 -->|Accept| OPT & STR
  • Composite Pattern menjelaskan struktur expression tree — non-terminal expressions adalah Composite, terminal expressions adalah Leaf
  • Interpreter Pattern menjelaskan evaluasiInterpret() method yang berjalan rekursif seperti operasi Composite
  • Visitor Pattern bisa ditambahkan untuk operasi lain pada tree yang sama — optimisasi, pretty-print, serialisasi — tanpa mengubah Expression class

Kapan Menggunakan dan Kapan Tidak #

GUNAKAN Interpreter jika:
  ✓ Perlu menginterpretasikan bahasa sederhana atau DSL (Domain Specific Language)
  ✓ Grammar-nya relatif sederhana dan aturannya stabil
  ✓ Perlu mendefinisikan query atau ekspresi secara dinamis di runtime
  ✓ Membangun rule engine, filter system, atau expression evaluator
  ✓ Ekspresi perlu disimpan di database dan dievaluasi nanti

HINDARI Interpreter jika:
  ✗ Grammar sangat kompleks — gunakan parser generator (ANTLR, PEG)
  ✗ Performa adalah prioritas — tree traversal lebih lambat dari compiled approach
  ✗ Bahasa yang diinterpretasikan sudah ada library-nya (SQL: sqlx, math: expr)
  ✗ Grammar sering berubah — setiap perubahan grammar bisa break banyak class

Kapan Pakai Library vs Implementasi Sendiri

Interpreter Pattern cocok untuk DSL sederhana yang kamu kontrol penuh. Untuk kebutuhan yang lebih kompleks, pertimbangkan: github.com/expr-lang/expr untuk expression evaluation, github.com/antlr4-go/antlr/v4 untuk grammar kompleks, atau github.com/nicholasgasior/gsfmt untuk template. Interpreter Pattern tetap berguna untuk memahami bagaimana library-library ini bekerja di balik layar.


Checklist Review Interpreter #

GRAMMAR:
  □ Grammar sudah didefinisikan secara eksplisit (BNF atau EBNF)
  □ Setiap aturan grammar direpresentasikan sebagai Expression class
  □ Terminal expression adalah leaf — tidak punya Expression lain di dalamnya
  □ Non-terminal expression mendelegasikan ke child expression secara rekursif

PARSER:
  □ Lexer memecah input menjadi token-token dengan benar
  □ Parser mengimplementasikan recursive descent sesuai grammar
  □ Error reporting jelas — posisi dan token yang menyebabkan error
  □ Parser menangani edge case (string kosong, ekspresi tunggal)

EVALUASI:
  □ Short-circuit evaluation diimplementasikan untuk AND dan OR
  □ Error dari child expression di-propagate dengan konteks yang jelas
  □ Context mendukung nested field access (dot notation)
  □ Type mismatch menghasilkan error, bukan panic

TESTING:
  □ Setiap terminal expression ditest secara terisolasi
  □ Setiap non-terminal expression ditest dengan mock child
  □ Parser ditest untuk query kompleks dengan berbagai operator
  □ Edge case: field tidak ada, type mismatch, ekspresi kosong

Ringkasan #

  • Interpreter mengubah bahasa menjadi pohon objek yang bisa dievaluasi — setiap aturan grammar menjadi Expression class; kalimat dalam bahasa menjadi Expression tree.
  • Dua jenis expression: terminal (leaf — nilai atau field tanpa child) dan non-terminal (composite — mengandung child expression yang dievaluasi rekursif).
  • Double komponen: Lexer memecah string menjadi token; Parser mengubah token menjadi Expression tree menggunakan recursive descent.
  • Short-circuit evaluation penting untuk AND dan OR — jika left operand sudah menentukan hasil, right operand tidak perlu dievaluasi.
  • Filter query engine adalah use case paling umum — query seperti status == "active" AND age > 25 di-parse menjadi AndExpression yang berisi EqualsExpression dan GreaterThanExpression.
  • Cache parsed expression — parsing adalah operasi mahal; jika query yang sama digunakan berulang kali, simpan hasilnya di cache.
  • Kombinasi natural: Interpreter menggunakan Composite untuk struktur tree; Visitor bisa ditambahkan untuk operasi lain (optimisasi, serialisasi) tanpa mengubah Expression class.
  • Untuk grammar kompleks, gunakan parser generator — Interpreter Pattern cocok untuk DSL sederhana yang terkontrol, bukan untuk bahasa pemrograman penuh.

← Sebelumnya: Visitor   Berikutnya: Thread Pool →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact