Search code examples
ruststrategy-patterndynamic-dispatch

Design patterns without the box


Rust beginner here. I have a number of algorithms that are almost identical but, at the final step, they all aggregate the results in slightly differently ways. Let's say the Algorithm does the following:

pub struct Algorithm<T> {
    result_aggregator: Box<dyn ResultAggregator<T>>,
}

impl<T> Algorithm<T> {
    pub fn calculate(&self, num1: i32, num2: i32) -> T {
        let temp = num1 + num2;
        self.result_aggregator.create(temp)
    }
}

With this, I can create a few different result aggregator classes to take my temp result and transform it into my final result:

pub trait ResultAggregator<T> {
    fn create(&self, num: i32) -> T;
}

pub struct FloatAggregator;
pub struct StringAggregator;

impl ResultAggregator<f32> for FloatAggregator {
    fn create(&self, num: i32) -> f32 {
        num as f32 * 3.14159
    }
}

impl ResultAggregator<String> for StringAggregator {
    fn create(&self, num: i32) -> String {
        format!("~~{num}~~")
    }
}

...and call it like so:

fn main() {
    // Here's a float example
    let aggregator = FloatAggregator;
    let algorithm = Algorithm {
        result_aggregator: Box::new(aggregator),
    };

    let result = algorithm.calculate(4, 5);
    println!("The result has value {result}");
    
    // Here's a string example
    let aggregator = StringAggregator;
    let algorithm = Algorithm {
        result_aggregator: Box::new(aggregator),
    };

    let result = algorithm.calculate(4, 5);
    println!("The result has value {result}");
}

This is what I've come up with.

Question: Is it possible to do this without the dynamic box? It's performance critical and I understand that generics are usually a good solution but I've had no luck figuring out how to get it working without dynamic dispatch.

So what's the Rusty solution to this problem? I feel like I'm approaching it with my C# hat on which is probably not the way to go.

Link to the playground


Solution

  • You can use an associated type instead of a generic parameter:

    pub trait ResultAggregator {
        type Output;
        fn create(&self, num: i32) -> Self::Output;
    }
    
    pub struct FloatAggregator;
    pub struct StringAggregator;
    
    impl ResultAggregator for FloatAggregator {
        type Output = f32;
        fn create(&self, num: i32) -> f32 {
            num as f32 * 3.14159
        }
    }
    
    impl ResultAggregator for StringAggregator {
        type Output = String;
        fn create(&self, num: i32) -> String {
            format!("~~{num}~~")
        }
    }
    
    pub struct Algorithm<Aggregator> {
        result_aggregator: Aggregator,
    }
    
    impl<Aggregator: ResultAggregator> Algorithm<Aggregator> {
        pub fn calculate(&self, num1: i32, num2: i32) -> Aggregator::Output {
            let temp = num1 + num2;
            self.result_aggregator.create(temp)
        }
    }