Summary
My previous article extracted conditions into functions on simple cases. Harder situations are not solved by an extraction, and this one takes such a case: evaluating an expression like 1 + (6 + 5) * 8 while respecting associativity and operator precedence.
The first solution is the one everybody writes: an Expression class with an operator, a left operand and a right operand, and a switch to evaluate. It works, and it breaks the open/closed principle, since every new operator forces a change to the switch. Polymorphism fixes that: an abstract Expression, a ValueExpression for values, one OperatorExpression per operator, and a new operator becomes a new class that touches none of the others.
But polymorphism alone does not hold against a deeply nested expression. Hence the interpreter pattern, laid on top: a Lexer that splits the string into tokens, a Parser that reorders them by precedence with an algorithm inspired by merge sort, and an Interpreter that evaluates them with a stack. The strategy pattern has not gone; it still evaluates each operation through a Map of operators.
What made the whole journey possible were the Jest tests written first and updated at every step: no change is accepted unless they pass. The repository keeps one branch per step.
Key ideas
- A switch in the evaluator breaks the open/closed principle: every new operator changes existing code.
- Polymorphism, one class per operator, makes adding an operator trivial and removes the switch.
- Faced with nested expressions, a lexer, a parser and an interpreter take over, and the strategy pattern stays beneath the interpreter.
- Jest expectations, written first and updated at every step, are what make the refactoring verifiable.
Why I wrote this
In my previous article we had explored how to extract conditions into functions on simple, practical cases. More complicated situations are not solved by simple extractions.