In this lesson, you will learn how to decouple your tree-traversal logic from your data structures using the Trait-based visitor pattern.
The Problem: Rigid Data
Imagine you have a document structure made of different types, like Text, Heading, and Image. If you add a method fn render() directly to these types, your data structure becomes cluttered with presentation logic. If you later decide to add an export_to_json() feature, you have to modify every single struct again. This violates the "Open-Closed" principle.
The Visitor Solution
Instead of forcing the nodes to know how to "render" or "export," we create a Visitor trait. This trait defines a "visit" method for every type in your tree. The data nodes don't perform the action themselves; they simply "accept" a visitor and call the appropriate method on it.
trait Visitor {
fn visit_text(&mut self, text: &str);
fn visit_image(&mut self, path: &str);
}
trait Element {
fn accept(&self, visitor: &mut dyn Visitor);
}
struct Text(String);
impl Element for Text {
fn accept(&self, visitor: &mut dyn Visitor) {
visitor.visit_text(&self.0);
}
}
Decoupling Logic
Now, your data structures only know how to route the visitor. You can create as many separate implementations of Visitor as you want without ever touching the Text or Image structs again.
If you want to create a HtmlRenderer, you implement Visitor to convert types into tags. If you want a WordCounter, you implement Visitor to simply increment a integer. By using a trait, the tree structure acts as a stable host, and the Visitor implementations act as swappable plugins that define the behavior for each node type. This pattern is particularly powerful for complex ASTs (Abstract Syntax Trees) where you want to perform multiple, distinct passes over the same data without polluting your core models.
Key Takeaway: The visitor pattern offloads traversal logic to an external trait, allowing you to add new operations to complex data structures without modifying the underlying types.