
Start with the design patterns overview · Part 15 of 23 · Behavioral patterns
Represent a small grammar as objects that evaluate expressions in a context. This article follows the example in my 23 Design Patterns repository, connecting the problem, participating classes, Python and C++20 implementations, and the trade-offs that decide whether to use it.
The Problem
Permission rules combine named roles and conjunctions, and rules should be built as data structures.
Naive Solution
bool allowed = roles.contains("editor") && roles.contains("verified");
Why It Becomes a Problem
One hardcoded boolean expression is simple but changing nested rule structures requires changing application code.
The Idea
Role is a terminal expression. Both is a nonterminal that evaluates two child expressions with short-circuit AND.
Reading the illustration
A phrasebook contains rules for combining known words. Each grammar rule knows how to interpret its part of the sentence.
In the repository example, the same design idea addresses this software problem: Permission rules combine named roles and conjunctions, and rules should be built as data structures.
Structure
Diagram · Python source · C++20 source
Context --> Both(Expression, Expression) --> Role / nested Both
Participants
Expression defines evaluation, Context supplies roles, Role tests membership, Both owns its child expressions.
Canonical roles in this example:
Abstract Expression— The contract for evaluating nodes in an Interpreter grammar. Here:Expression.Terminal Expression— An expression with no child expressions. Here:Role.Nonterminal Expression— An expression that combines child expressions according to a grammar rule. Here:Both.Context— The evaluation data used by expressions; here it is the set of role names.Context.
Python Example
The complete Python source is shown first.
class Role:
def __init__(self, name):
self.name = name
def evaluate(self, context):
return self.name in context
class Both:
def __init__(self, left, right):
self.left = left
self.right = right
def evaluate(self, context):
return self.left.evaluate(context) and self.right.evaluate(context)
if __name__ == "__main__":
rule = Both(Role("editor"), Role("verified"))
for context in [set(), {"editor"}, {"editor", "verified"}]:
print(rule.evaluate(context))
Python Output
False
False
True
Code Walkthrough
Expression defines evaluation, Context supplies roles, Role tests membership, Both owns its child expressions.
Start at the final call in the Python example. Follow the middle role in the diagram and compare how the C++20 version handles the same responsibility.
C++20 Example
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <unordered_set>
#include <utility>
using Context = std::unordered_set<std::string>;
struct Expression {
virtual ~Expression() = default;
virtual bool evaluate(const Context& context) const = 0;
};
class Role final : public Expression {
std::string name_;
public:
explicit Role(std::string name) : name_(std::move(name)) {}
bool evaluate(const Context& context) const override { return context.contains(name_); }
};
class Both final : public Expression {
std::unique_ptr<Expression> left_, right_;
public:
Both(std::unique_ptr<Expression> left, std::unique_ptr<Expression> right)
: left_(std::move(left)), right_(std::move(right)) {
if (!left_ || !right_) throw std::invalid_argument("Missing expression");
}
bool evaluate(const Context& context) const override {
return left_->evaluate(context) && right_->evaluate(context);
}
};
int main() {
const Both rule{std::make_unique<Role>("editor"), std::make_unique<Role>("verified")};
std::cout << std::boolalpha << rule.evaluate(Context{"editor"}) << '\n';
std::cout << rule.evaluate(Context{"editor", "verified"}) << '\n';
}
C++20 Output
false
true
When to Use
Use it for a small stable grammar whose expression tree is useful to construct and inspect.
Use cases
Small filtering or eligibility languages fit; this is not a secure authorization system or a general parser.
When NOT to Use
Avoid it for a large language needing robust parsing, diagnostics and optimization; established parser tools are more appropriate.
Advantages
Rules compose recursively and can be evaluated against different contexts.
Trade-offs
Each grammar form adds code. Deep trees risk stack exhaustion, and a parser is deliberately absent: main constructs the syntax tree directly.
Related Patterns
Common Confusion
Composite describes the tree structure; Interpreter adds grammar-specific meaning and evaluation. Visitor can add operations over that tree.
Terms to Remember
Interpreter— Represent a small language as objects that evaluate its grammar rules.Abstract Expression— The contract for evaluating nodes in an Interpreter grammar. Example:Expression.Terminal Expression— An expression with no child expressions. Example:Role.Nonterminal Expression— An expression that combines child expressions according to a grammar rule. Example:Both.Context— The evaluation data used by expressions; here it is the set of role names.
Interview Vocabulary
abstract syntax tree— A tree representing grammatical structure rather than the original text’s surface formatting.recursive composition— Building a structure from parts that expose the same contract as the whole.short-circuit evaluation— Skipping later operands when an earlier result already determines the answer.
Interview Question
Where would precedence be handled if users typed editor AND verified OR admin?
Mini Challenge
Add Either for OR and test a nested rule with three distinct contexts.
Compare the two versions
Both examples build an expression tree directly; neither parses text. Python uses a set as Context and matching evaluate methods. C++ declares an Expression Interface. Use a direct boolean expression when rules do not need to be represented as data. The two examples express the same pattern responsibility; compare their setup and output before changing an input.
Check yourself
- Does this example parse text, or evaluate an already built tree?
- When would the naive solution on this page be easier to maintain? Give a concrete example.
- Change one input in the Python example. Predict the output and explain which responsibility handles the change.
Run and explore the example
The Python code comes from python/main.py, with expected output. The C++20 code comes from behavioral/interpreter/cpp/main.cpp. Follow the repository’s C++20 build instructions to compile it and compare the result with expected.txt. The output demonstrates this example’s behavior; it does not cover every input or the challenge above.
The source example and adapted explanation are © 2026 Sanfor2004, provided under the MIT license. The sketchbook cover is an illustration preserved from this site’s original pattern lessons.
Continue the series
- Overview: all 23 patterns, their categories, and how to choose
- Previous: Command (Behavioral Pattern)
- Next: Iterator (Behavioral Pattern)