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 evaluasi —
Interpret()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/expruntuk expression evaluation,github.com/antlr4-go/antlr/v4untuk grammar kompleks, ataugithub.com/nicholasgasior/gsfmtuntuk 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 > 25di-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.