Sling Academy
Home/Rust/Implementing the Visitor Pattern with Enums and Traits in Rust

Implementing the Visitor Pattern with Enums and Traits in Rust

Last updated: January 06, 2025

The Visitor Pattern is a popular design pattern used to separate algorithms from the structures they operate on. In Rust, a statically typed language, we can implement this pattern using enums and traits to achieve both type safety and behavioral extension without altering existing structures.

Overview

In traditional object-oriented languages, the Visitor Pattern involves declaring an interface representing a visitor, which then alters the operational behavior of the elements it visits. Rust achieves this by leveraging traits to define behavior and enums to handle variants of structures.

Defining Elements

First, we need an enum representing different elements. Consider a simple geometric example:

enum Shape {
    Circle(f64),  // stores radius
    Rectangle(f64, f64), // stores width and height
}

Each shape in this context could be a geometric figure requiring a visit operation.

Defining the Visitor Trait

The next step is to define a visitor trait that declares a method for each type of element:

trait Visitor {
    fn visit_circle(&self, radius: f64);
    fn visit_rectangle(&self, width: f64, height: f64);
}

The trait here outlines the operations we want to perform on each shape.

Implementing the Visitor Pattern

To implement this pattern, each element type must have a method to accept a visitor. This means adding a method to our enum:

impl Shape {
    fn accept(&self, visitor: &dyn Visitor) {
        match &self {
            Shape::Circle(radius) => visitor.visit_circle(*radius),
            Shape::Rectangle(width, height) => visitor.visit_rectangle(*width, *height),
        }
    }
}

Here, the accept method on Shape simply calls the appropriate visit method based on the shape type.

Example Visitor Implementation

We can now create concrete visitors implementing the Visitor trait to define behavior. Let us create a visitor that calculates and prints the area of each shape.

struct AreaCalculator;

impl Visitor for AreaCalculator {
    fn visit_circle(&self, radius: f64) {
        let area = std::f64::consts::PI * radius * radius;
        println!("Circle area: {:.2}", area);
    }
    
    fn visit_rectangle(&self, width: f64, height: f64) {
        let area = width * height;
        println!("Rectangle area: {:.2}", area);
    }
}

Testing the Implementation

To test our visitor, we create some shape instances and invoke the visitor methods:

fn main() {
    let shapes: Vec = vec![Shape::Circle(5.0), Shape::Rectangle(3.0, 4.0)];
    let area_calculator = AreaCalculator;

    for shape in shapes {
        shape.accept(&area_calculator);
    }
}

The main function demonstrates creating instances of Shape and calling their accept method with an AreaCalculator instance, which visits each element and computes the area.

Advantages and Uses

Implementing the Visitor Pattern in Rust using enums and traits allows for clean separation between data structure and operations. This differentiation leads to the enhancement of a program’s maintainability—new operations or transformations can be added without altering existing structure code, reinforcing the Open/Closed Principle in design.

The technique is useful for scenarios requiring operations over heterogeneously structured data, such as different shape calculations, command parsing, and more. By leveraging Rust’s strong type system, patterns like Visitor gain safety and expressive power.

Next Article: Designing Object-Like APIs in Rust for Safety and Performance

Previous Article: How Rust’s Borrow Checker Affects Object-Like Design

Series: Object-Oriented Programming in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior