10.3 Traversals, Snapshots, External Operations, and Small Languages
The final batch of behavioral challenges in the meeting hall stem from four closely related but fundamentally distinct concerns: collection traversal, undo history, syntax tree manipulation, and rule interpretation. At first glance, they appear similar, but their evolution and implementation paths diverge significantly.
These four behavioral patterns are often hidden behind the scenes in languages and standard libraries: for-each relies on iterators, editor history echoes the Memento pattern, compilers frequently use visitors to traverse syntax trees, and rule DSLs typically employ interpreters. Understanding the boundaries of these patterns is more important than manually coding boilerplate solutions.
Iterator: Expose the Traversal Protocol, Not the Storage Structure
public final class MatchHistory implements Iterable<MatchSummary> {
private final java.util.List<MatchSummary> matches;
public MatchHistory(java.util.List<MatchSummary> matches) {
this.matches = java.util.List.copyOf(matches);
}
@Override
public java.util.Iterator<MatchSummary> iterator() {
return matches.iterator();
}
}Consumers can iterate using for-each, without needing to know whether the internal structure is an array, a tree, or a paginated cursor.
When designing an iterator, you must clearly define:
- Whether the traversal order is stable;
- Whether iteration can be repeated or is one-time and consumes the data;
- Whether modifications to the collection are allowed during traversal;
- Whether the iterator provides a snapshot or a live view;
- How to report failures in lazy loading;
- Who is responsible for closing database cursors, file handles, or other resources.
Java's fail-fast behavior typically only attempts to detect concurrent modifications, it does not guarantee thread safety. For traversals across I/O operations, it's preferable to return a closeable Stream/cursor, and the caller should use try-with-resources to manage resource cleanup.
Memento: Save and Restore State While Preserving Encapsulation
Memento enables the originator to create opaque snapshots, which are then stored and restored by the caretaker:
public final class TournamentEditor {
public record Snapshot(
String name,
java.util.List<RoundDraft> rounds,
long revision) {
public Snapshot {
rounds = java.util.List.copyOf(rounds);
}
}
public Snapshot snapshot() {
return new Snapshot(name, rounds, revision);
}
public void restore(Snapshot snapshot) {
this.name = snapshot.name();
this.rounds = new java.util.ArrayList<>(snapshot.rounds());
this.revision = snapshot.revision();
checkRep();
}
}Snapshots should contain the complete state necessary to restore invariants and must sever mutable aliases. They may be large, so incremental snapshots, command history, or persistent checkpoints can be considered, though only after measurement.
Memento differs from Event Sourcing: Memento preserves the state of a specific moment; Event Sourcing treats domain events as authoritative facts and derives state by replaying those events. The latter requires event versioning, ordering, and an immutable log, conditions that cannot be met by simply using List<Snapshot>.
Snapshots may contain personal information, cryptographic keys, or previously deleted data, necessitating encryption, access controls, and retention policies.
Visitor: Adding New Operations to Stable Element Families
When the set of element types is stable but new operations are frequently added, the Visitor pattern moves operations out of element classes and preserves type-specific information through double dispatch.
public sealed interface Reward
permits Coins, ItemReward {
<R> R accept(RewardVisitor<R> visitor);
}
public interface RewardVisitor<R> {
R visitCoins(Coins coins);
R visitItem(ItemReward item);
}
public record Coins(Money amount) implements Reward {
@Override
public <R> R accept(RewardVisitor<R> visitor) {
return visitor.visitCoins(this);
}
}New capabilities (such as valuation, export, or auditing) can be added as Visitor implementations without modifying the dispatch logic of each operation.
The trade-off is reversed: adding a new Reward requires modifying all existing Visitors. If element types are frequently added while operations remain few, it's better to keep behavior within the element classes. Java's sealed types with exhaustive pattern matching offer a more concise alternative for external operations. The choice ultimately depends on language version, module boundaries, and the direction of future expansion.
Visitors should not require elements to expose all private fields. Elements should provide only stable, operation-specific queries, maintaining encapsulation.
Interpreter: Building a Controlled Evaluator for Finite Grammars
When business logic requires expressing rules, such as:
level >= 18 AND region IN ("ap", "eu")don’t hand the string to a generic eval. A controlled interpreter typically follows four steps:
Characters -> Tokens -> AST -> Evaluate in a bounded contextpublic sealed interface EligibilityExpr {
boolean evaluate(EligibilityContext context);
}
public record And(EligibilityExpr left, EligibilityExpr right)
implements EligibilityExpr {
@Override
public boolean evaluate(EligibilityContext context) {
return left.evaluate(context) && right.evaluate(context);
}
}
public record MinimumLevel(int level) implements EligibilityExpr {
@Override
public boolean evaluate(EligibilityContext context) {
return context.level() >= level;
}
}The AST nodes compose the grammar, and the evaluation context exposes only permitted data.
A production DSL must also enforce:
- Maximum input size, AST depth, and execution time;
- Whitelisted fields and functions;
- Numeric precision, time zones, and null semantics;
- Error location and diagnostics;
- Grammar and rule versioning, with backward compatibility;
- Malicious regular expressions, recursion, or resource exhaustion.
When the grammar becomes moderately complex, use a mature parser generator or parsing library, rather than continually expanding split() and regular expressions. The interpreter pattern describes collaboration between the AST and evaluation context; it does not require hand-rolling a lexer or parser.
Four Pattern Selection Table
| Change Question | Pattern |
|---|---|
| Separation of traversal mechanism and collection representation | Iterator |
| Saving and restoring an object's internal state | Memento |
| Stable element family with frequent external operations added | Visitor |
| Finite grammar requiring parsing and safe evaluation | Interpreter |
Behavior Pattern Total Check
Before selecting any behavioral pattern, answer these questions:
- Is it the algorithm, workflow, state, notification, or traversal pattern that's changing?
- What are the semantics around synchronization, asynchrony, ordering, errors, and cancellation?
- Who owns object state and resources?
- Do retry, replay, or recovery operations require idempotency and versioning?
- Are built-in language features (such as functions, sealed switches, Iterable, or Stream) already sufficient?