Replace Implicit Language With Interpreter

Destination
Compare
Symptom
Human

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.

Agent

String-DSL parsing and dispatch entangled in one method body the agent must trace per call. The grammar is implicit in regex patterns and conditionals; the agent cannot statically enumerate valid expressions or verify that all paths handle malformed input.

Goal
Human

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.

Agent

Each grammar node is one class the agent verifies independently. The composed expression tree is a statically-typed data structure the agent reads structurally; per-node tests pin per-node behaviour.

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
Human

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.

Agent

Implicit DSLs defeat static analysis at the string boundary — the agent's verification cost spans both parser and evaluator. Adding a syntactic form requires the agent to coordinate parser, evaluator, and tests; misalignments between the three are silent and ship to runtime.

Tradeoff
Human

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.

Agent

Interpreter spreads a single conceptual query across multiple class files; the agent traverses the tree to know what an expression does. Static call-graph analysis loses precision on polymorphic `matches` calls — the agent must enumerate concrete node types.

Relief
Human

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.

Agent

Adding a new operator is one new class in the grammar hierarchy; the agent enumerates operators by listing the classes that implement the interface, and tests target evaluation against the typed AST instead of raw string parsing.

Trap
Human

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.

Agent

A grammar with many nodes representing fine-grained syntactic variations forces the agent to load a large hierarchy to reason about any expression. Per-node specialization (NumericEquals vs. StringEquals vs. DateEquals) can multiply file count without proportional reasoning gain.