Symptom

A small but stable mini-language is embedded as strings in code, parsed and evaluated by ad-hoc functions that grow operator-by-operator. Operator precedence is implicit in eval order; new operators require editing the parser AND the evaluator; static analysis of rules is impossible because they're untyped strings.

Goal

Each grammar rule becomes a class with an interpret(context) method. The AST is the program; building it is explicit (constructors), evaluating it is recursive descent through interpret(). New operators are new classes; the parser (if needed) and the evaluator stay structurally separate.

Before the pattern

function evaluateRule(ruleString, context) {
if (ruleString.includes(' AND ')) {
const [left, right] = ruleString.split(' AND ');
return evaluateRule(left, context) && evaluateRule(right, context);
}
if (ruleString.includes(' OR ')) {
const [left, right] = ruleString.split(' OR ');
return evaluateRule(left, context) || evaluateRule(right, context);
}
if (ruleString.startsWith('NOT ')) {
return !evaluateRule(ruleString.slice(4), context);
}
return Boolean(context[ruleString]);
}
const pass = evaluateRule('verified AND NOT banned', user);
// Strings as DSL, ad-hoc parser, operator precedence implicit in eval order,
// no caching, no static analysis, every consumer re-implements its own dialect.

After the pattern

class Variable {
constructor(name) {
this.name = name;
}
interpret(env) {
return Boolean(env[this.name]);
}
}
class And {
constructor(left, right) {
this.left = left;
this.right = right;
}
interpret(env) {
return this.left.interpret(env) && this.right.interpret(env);
}
}
class Or {
constructor(left, right) {
this.left = left;
this.right = right;
}
interpret(env) {
return this.left.interpret(env) || this.right.interpret(env);
}
}
class Not {
constructor(operand) {
this.operand = operand;
}
interpret(env) {
return !this.operand.interpret(env);
}
}
const rule = new And(new Variable('verified'), new Not(new Variable('banned')));
const pass = rule.interpret(user);
Example source: Illustrative example written for this site in the spirit of Design Patterns (Gamma, Helm, Johnson, Vlissides, Addison-Wesley, 1994), chapter 5. The book's running example is a regular-expression matcher; this JavaScript adaptation uses a tiny boolean rule language (AND, OR, NOT, variables) because the AST is small enough to read in one screen and the structural-vs-string-DSL contrast is sharp.
Pressure

String-DSL rules are Primitive Obsession at the worst level — the type system sees a string; the runtime expects a domain language. Every consumer either re-implements the parser or imports it; bugs in the embedded parser silently corrupt evaluation across every consumer in lockstep.

Tradeoff

Building the AST in client code is verbose: new And(new Variable('a'), new Or(...)) is harder to read than 'a AND (b OR c)'. The pattern shines when the system controls rule construction (rules come from a config file or builder); it strains when human-readable input is required and a parser is needed anyway.

Relief

Operator semantics live in one class per operator; new operators are isolated changes; AST is statically typed and traversable. Static analysis (which variables does this rule depend on?) becomes a single recursive walk of the AST.

Trap

Grammars that grow beyond a handful of operators outgrow the pattern fast — every new operator adds a class, every binary operator with precedence needs explicit AST shape, and traversal logic accumulates. At that point reach for a real parser generator or a proper AST-walker library; don't keep adding interpret() methods to a sprawling class hierarchy.