Replace Implicit Language With Interpreter

Destination
Symptom

Domain expressions are encoded as strings — `'tag=urgent AND priority>3'`, `'$.user.profile.name'` — that the system parses on every use. The grammar lives in comments and parser code; valid expressions are runtime concerns; type checking is impossible; combinators (AND/OR/NOT) bolt on with growing string-manipulation complexity.

Goal

Each grammatical element is a domain object: `Equals`, `GreaterThan`, `And`, `Or`. Expressions are built by composition; evaluation is a polymorphic `matches(context)` call. The grammar is the type system; composition replaces parsing.

Before the refactoring

// A string-based DSL carries the filter; parsing and dispatch are coupled in one method.
class TaskFilter {
matches(task, expression) {
// expression like 'tag=urgent AND priority>3'
const clauses = expression.split(' AND ');
return clauses.every((clause) => {
const equalsMatch = clause.match(/^(\w+)=(\S+)$/);
if (equalsMatch) return task[equalsMatch[1]] === equalsMatch[2];
const greaterMatch = clause.match(/^(\w+)>(\S+)$/);
if (greaterMatch) return task[greaterMatch[1]] > Number(greaterMatch[2]);
throw new Error(`unparseable clause: ${clause}`);
});
}
}

After the refactoring

// Domain objects compose into an expression tree; each node knows how to evaluate itself.
class Equals {
constructor(field, value) { this.field = field; this.value = value; }
matches(task) { return task[this.field] === this.value; }
}
class GreaterThan {
constructor(field, value) { this.field = field; this.value = value; }
matches(task) { return task[this.field] > this.value; }
}
class And {
constructor(left, right) { this.left = left; this.right = right; }
matches(task) { return this.left.matches(task) && this.right.matches(task); }
}
class Or {
constructor(left, right) { this.left = left; this.right = right; }
matches(task) { return this.left.matches(task) || this.right.matches(task); }
}
const filter = new And(new Equals('tag', 'urgent'), new GreaterThan('priority', 3));
tasks.filter((task) => filter.matches(task));
Example source: Illustrative example written for this site, faithful to Kerievsky's pattern shape in Refactoring to Patterns (Addison-Wesley, 2004), chapter 11. Per ADR-0004, the bookend contrasts the implicit string DSL with the explicit AST — the intermediate grammar-extraction steps live in the linked Fowler refactorings. The book uses a query-language example; this JavaScript version uses task-filtering predicates.
Pressure

Implicit DSLs are Primitive Obsession at scale — strings carry domain semantics the type system can't enforce. Parser bugs ship as 'this should match but doesn't'; new operators require parser edits; tests must cover both parsing and evaluation; the two concerns tangle.

Tradeoff

Interpreter introduces a class per grammar node. For two or three operators with a stable grammar, the inline string-and-conditional form is often cheaper than the class hierarchy. The composed form is more verbose at the construction site than the string form.

Relief

The grammar is statically typed and locally readable. Adding an operator is one new class; combinators are composable; tests target one node at a time. The expression's structure is visible in source rather than encoded in a string.

Trap

An Interpreter applied to a grammar that's actually trivial (two operators, stable, low surface) is ceremony — readers must navigate a class hierarchy to understand what would have been three lines of string-matching. The pattern earns its keep when the grammar grows and composition matters.