I have this situation: Our model includes a struct called Order, that is used very heavily in the codebase.
So we have many, many function signatures of the form order: Order or order: &Order, etc.
The system now requires multiple types of Order. Let's call one "subclass" (!) SpreadOrder. Were this Scala, I'd have trait Order with class SpreadOrder: Order.
Of course, Rust works differently. Options I'm considering:
Converting Order to a trait
Converting Order to an enum - this hierarchy is essentially a sum ADT
I wanted to avoid large-scale conversion of function signatures but that is probably not possible. I want to implement the best solution, even if the conversion is painful.
Other things to note:
Order has many fields and methods. I don't want any solution that involves some kind of wrapper with delegation.
I do not need dynamic dispatch
The problem with the enum solution
pub enum Order {
Single(SingleOrder),
Spread(SpreadOrder),
}
is that the large number of base Order fields have to be duplicated in Spread (and other variants).
The problem with a trait-based solution is that, since Rust lacks implementation inheritence, the base set of fields/methods would need to be repeated (whether or not we wrap some BaseOrder struct that contains the common fields/methods).
I think there is probably no way to get around some kind of wrapper with delegation. But I would love to get input.
I realize this is a rather open-ended modeling question. Again, just looking for some input.
If the set of types that classify as Order is closed (i.e. you don't need to support your users adding their own types that classify as Order), I'd go with the enum. As for de-duplicating fields, I'd keep it simple and have a third type OrderCommon or something, which SingleOrder and SpreadOrder safe in a field. E.g.:
pub enum Order {
Single(SingleOrder),
Spread(SpreadOrder),
}
struct OrderCommon {
// common fields for each order
}
pub struct SingleOrder {
common: OrderCommon,
// fields specific for a single order
}
pub struct SpreadOrder {
common: OrderCommon,
// fields specific for a spread order
}
That's the critical part that would decide everything else and which is not revealed, as usual. Just why system, suddenly, “requires multiple types of Order”. Where that requirement comes from? What do you want to achieve there? What's the final goal?
We have delegation for all the base functionality, but that's really not too painful.
This is may be the closest analog in Rust to the common implementation inheritance scenario of base class + subclasses that add small bits that doesn't involve a lot of duplication.
(I now regret ever using public fields on a struct that is really meant as a class – not just a simple data container. I will have to replace all the direct field access with methods. It's kind of a shame that Rust makes you write accessors by hand, but not too big of a deal.)
Sure it is, because it affects what other kinds of orders are likely to happen in the future. Are you sure you're not going to hit https://wiki.c2.com/?LimitsOfHierarchies problems, for example? Maybe you actually want an ECS so that you can add lots of different optional components to orders, for example, since it might be that some orders need multiple of those components.
It's really really hard to abstract well from two examples.
struct Order {
// many fields
}
struct SpreadOrder {
order: Order,
// extra fields
}
it's all trade-offs at the end of day, just like any programming problems, but even in traditional OOP languages, composition is usually favored over inheritance by many experts. you can find many interesting articles on this topic by simply search the keyword "composition inheritance".
rust also has traits like Into, AsRef, Borrow to help converting between types. note: using Deref to emulate OO style "inheritance" is possible to certain extent, but it is deemed an anti-pattern by the community in general, just be aware.
one question worth asking: do the existing functions that take Order or &Order as argument need to be updated to handle the new SpreadOrder, or only new functions are added for SpreadOrder?
easy to write, with minimal duplication.
I believe this is the pattern I will use going forward to mimic OOP-style inheritance (which, sometimes, is appropriate).
I about agree with the structure of your transition, though in the case I did it a bit differently and I covered the boilerplate with a macro (see below).
I preferred wrapping each variant in a struct so that when I know I have exactly one of them, I can represent that in the type system (I needed that). I also added From/Into conversions from variants into the main type.
I did not use functional-style mutators like you did, but consider taking the mutator argument as self rather than &self to make it more likely for the compiler to be able to optimize it with an in-place mutation, just in case any fields are not copy. Maybe not relevant for your specific fields?
I used AsRef (and, if you like, AsMut) instead of base() and implemented AsRef for all of them: Order, Single, Spread, OrderBase. That way, if I need something that just requires OrderBase to function, I can pass in O: AsRef<OrderBase> and have it be able to take any of them instead of having to manually write .base() to pass in OrderBase.
Controversial: I used the Deref-polymorphism antipattern and implemented Deref<Target=OrderBase> and DerefMut for all of them as well and stuck to accessing struct fields directly. That way, I can still access base fields directly (e.g. order.px in your case instead of a separate order.px() method) without invoking a method name. The reason for sticking the base fields in general is that I can take mutable references to multiple fields at once - which we can't do until we have view types.
That being said, mutable references to different fields doesn't work for OrderBase fields (e.g. you can't do &mut order.color and &mut order.px at once since the DerefMut invocation for &mut order.px locks the entire &mut order). So YMMV. You might choose not to use this pattern.
Anyway, this is the pattern I used translated to your code, using a macro to eliminate boilerplate while still making the call site look readable:
macro_rules! define_order {
(
$(#[$enum_meta:meta])*
$vis:vis enum Order {
$(
$(#[$variant_meta:meta])*
$variant:ident($struct:ident),
)*
}
) => {
$(#[$enum_meta])*
$vis enum Order {
$(
$(#[$variant_meta])*
$variant($struct),
)*
}
impl AsRef<OrderBase> for Order {
fn as_ref(&self) -> &OrderBase {
match self {
$(Order::$variant(v) => v.as_ref(),)*
}
}
}
// Controversial!
impl std::ops::Deref for Order {
type Target = OrderBase;
fn deref(&self) -> &OrderBase {
self.as_ref()
}
}
// And similar for AsMut, DerefMut
// Now the stuff for each variant struct.
$(
impl AsRef<OrderBase> for $struct {
fn as_ref(&self) -> &OrderBase {
&self.base
}
}
impl std::ops::Deref for $struct {
type Target = OrderBase;
fn deref(&self) -> &OrderBase {
&self.base
}
}
// And similar for AsMut, DerefMut
// Easy conversion from variant struct to Base.
impl From<$struct> for Order {
fn from(variant: $struct) -> Order {
Order::$variant(variant)
}
}
)*
}
}
define_order! {
#[doc="Documentation has to be written this way unfortunately."]
#[derive(Debug, Clone)]
pub enum Order {
// Note: the macro assumes `.base` is the base field in each struct!
#[doc="Variant's documentation."]
Single(Single),
#[doc="Variant's documentation."]
Spread(Spread),
}
}