inheritancestructrustcode-duplication

How to avoid code duplication of different structs with semantically equal fields/properties?


Given these two structs:

pub struct RectangleRenderer {
    canvas: Canvas,
    origin: Point,
    shape: Rectangle,
}

pub struct CircleRenderer {
    canvas: Canvas,
    origin: Point,
    shape: Circle,
}

As I come from Java, I would extract a base class ShapeRenderer out of those and apply the fields canvas and origin into that while the specific types will keep their field called shape. What's the best practice in Rust for this situation since traits only act similar to interfaces and therefore do not allow properties/fields?


Solution

  • This looks like a perfect case for generics.

    You can make a single struct like this:

    struct ShapeRenderer<T: Shape> {
        canvas: Canvas,
        origin: Point,
        shape: T,
    }
    

    Note that I have bounded the generic type T by a trait Shape (that you would have to create). You can put any bounds here you like (or no bounds at all), but you will be restricted to using members of those traits.

    Anything that you want to be able to access in your shapes would need to be exposed by Shape. For example, if you need the center and area, then the trait definition might look like this:

    trait Shape {
        fn center(&self) -> (f64, f64);
        fn area(&self) -> f64;
    }
    

    If that is not enough flexibility, you could also give ShapeRenderer special behavior for only specific shapes. For example:

    impl ShapeRenderer<Rectangle> {
        fn n_sides(&self) -> u32 {
            4
        }
    }
    

    Note that inside this impl, we have access to all the fields of Rectangle, not just the functions in Shape.


    Alternatively, you could create a base struct and then include it as a member of your final structs:

    struct Renderer {
        canvas: Canvas,
        origin: Point,
    }
    
    struct CircleRenderer {
        renderer: Renderer,
        shape: Circle,
    }
    
    struct RectangleRenderer {
        renderer: Renderer,
        shape: Rectangle,
    }
    

    This is the closest thing in Rust to standard inheritance.


    Thirdly, if you only care about code duplication when creating these structs and don't want them to share anything but fields, you could use a macro:

    macro_rules! make_renderer {
        ($name: ty, $shape: ty) => (
            struct $name {
                canvas: Canvas,
                origin: Point,
                shape: $shape,
            }
        );
    }
    
    make_renderer!(CircleRenderer, Circle);
    make_renderer!(RectangleRenderer, Rectangle);
    

    While the generics example is the most complicated, it is also the most powerful and flexible. It lets you easily have code shared between your structs, while also letting you have code specific to one that gives you access to all its fields.