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 ofVehicle
? - 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?
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:You can implement this trait on whatever types you want. If they need a common "base type", you can create one:
When a function needs to accept any vehicle, it will accept
&mut dyn Vehicle
orBox<dyn Vehicle>
(depending on whether it needs to borrow or take over the ownership of the vehicle).To answer your questions:
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 asself.common_data.num_wheels
.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 ofVehicle
for the concrete types could then forward their method implementations toself.base_vehicle.method()
, which would be the equivalent of asuper()
call. This approach adds a lot of boilerplate, so I wouldn't recommend it unless it actually makes sense, i.e. unless theBaseVehicle
actually offers a coherent and useful implementation ofVehicle
.