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.
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));
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.
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.
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.
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.