How to use composition instead of inheritance? Enum_dispatch?

1k Views Asked by At

I know I should use composition instead of inheritance.

In Java/C++, I have an abstract base class Vehicle (with properties and common methods) and classes which implement this, such as Car and Bike.

I've found enum_dispatch, which does a great job of forwarding method calls from the parent to the child "classes".

#[enum_dispatch]
pub trait VehicleDelegation {
    fn drive(&mut self) -> anyhow::Result<()>;
}

#[enum_dispatch(VehicleDelegation)]
pub enum Vehicle {
    Car,
    Bike,
}

pub struct Car {
    pub doors: u8
}

impl VehicleDelegation for Car {
    fn drive(&mut self) -> anyhow::Result<()> {
        // do car stuff
        Ok(())
    }
}

pub struct Bike {
    pub frame_size: u8
}

impl VehicleDelegation for Bike {
    fn drive(&mut self) -> anyhow::Result<()> {
        // do bike stuff
        Ok(())
    }
}
  • Where should I store common data, such as num_wheels which would have been properties of Vehicle?
  • Where should I define methods of Vehicle which use the common data?

At the moment I have to duplicate all of the Vehicle data and methods into every enum variant, which is getting tiresome as it scales with both the number of "methods" and the number of "classes".

  • How do I do this idiomatically in Rust?
1

There are 1 best solutions below

1
On

enum_dispatch is a crate that implements a very specific optimization, i.e. avoids vtable-based dynamic dispatch when the number of trait implementations is small and known in advance. It should probably not be used before understanding how traits work normally.

In Rust you start off with a trait, such as Vehicle, which would be the rough equivalent of a Java interface:

pub trait Vehicle {
    fn drive(&mut self) -> anyhow::Result<()>;
}

You can implement this trait on whatever types you want. If they need a common "base type", you can create one:

// Car and Bike can but don't have to actually use BaseVehicle in their
// implementations. As long as they implement the Vehicle trait, they're fine.

struct BaseVehicle {
    num_wheels: u8,
}

struct Car {
    base_vehicle: BaseVehicle,
    doors: u8,
}

impl Vehicle for Car {
    fn drive(&mut self) -> anyhow::Result<()> {
        // do car stuff
        Ok(())
    }
}

struct Bike {
    base_vehicle: BaseVehicle,
    frame_size: u8,
}

impl Vehicle for Bike {
    fn drive(&mut self) -> anyhow::Result<()> {
        // do bike stuff
        Ok(())
    }
}

When a function needs to accept any vehicle, it will accept &mut dyn Vehicle or Box<dyn Vehicle> (depending on whether it needs to borrow or take over the ownership of the vehicle).

To answer your questions:

  • Where should I store common data, such as num_wheels which would have been properties of Vehicle?

Wherever it's appropriate for your program. The typical approach, shown above, would be to put them in a "base" or "common" type that all vehicles include via composition. num_wheels, for example, is available as self.common_data.num_wheels.

  • Where should I define methods of Vehicle which use the common data?

Again, those would be defined on the common type where they could be accessed by specific types, and (if needed) exposed through their implementations of the trait.

If the "base" type is rich enough, it can itself implement the trait - in this case it would mean providing an impl Vehicle for BaseVehicle. The implementations of Vehicle for the concrete types could then forward their method implementations to self.base_vehicle.method(), which would be the equivalent of a super() call. This approach adds a lot of boilerplate, so I wouldn't recommend it unless it actually makes sense, i.e. unless the BaseVehicle actually offers a coherent and useful implementation of Vehicle.