0

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?
fadedbee
  • 42,671
  • 44
  • 178
  • 308
  • You could have a struct, one field of which is your `Vehicle` enum above; the other fields for the common data, and obvs an implementation on that struct for the common methods. – eggyal May 09 '21 at 06:56
  • @eggyal Thanks, I'd considered this, but I how can the `Vehicle` enum variants get access to their common parent's data? This needs to work for `mut`, so keeping references to the parent in the enum variants won't work. – fadedbee May 09 '21 at 06:58
  • 1
    I voted "needs details" but the real problem with this is that `Vehicle`/`Bike` etc. is a toy example; it's pure structure. It doesn't solve a *real problem*, and therefore it's impossible to make an informed recommendation about how to improve it. **You cannot divorce the design of a program from the problem it solves.** The main role of toy examples like this is to demonstrate *how inheritance works*, and that simply can't be done in Rust because Rust doesn't have inheritance. – trent May 09 '21 at 11:29
  • 1
    If you are having trouble with designing *some program that solves a problem*, and not just a toy program lifted from a Java book or whatever that demonstrates inheritance and not much else, in order to answer that question we would need the **requirements and constraints** that inform the design. Everything else is a waste of time. Bear in mind that most tiny, trivial programs are not complicated enough to need a complex feature like inheritance; the need emerges from complexity, so it's likely that the [mre] for an architectural problem is usually much more sizeable than for a syntax error. – trent May 09 '21 at 11:33
  • @trentcl Thanks, I agree with your point but see no obvious solution. I asked about a toy problem because I thought that posting hundreds of lines of domain-specific code would make the issue too hard to understand. (I think I've come up with a working solution, and I am intending to post it with a "toy" illustration, if it survives a few more days of dev.) – fadedbee May 09 '21 at 13:06
  • *I agree with your point but see no obvious solution.* - Is there a problem with the solution provided by my answer, though? That's how composition-based inheritance is typically done in Rust. If I have somehow misunderstood the question (for example, that the use of `enum_dispatch` is a requirement rather than an option, as suggested by the question's title), it would be fair to at least write a comment under the answer explaining how it misses the mark. – user4815162342 May 09 '21 at 17:44

1 Answers1

2

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.

user4815162342
  • 141,790
  • 18
  • 296
  • 355