In the context of a series of programming lessons, I have decided to use Rust instead of C++ as the support programming language. One aspect of these lessons is dedicated to OOP (dynamic dispatch) relying on interfaces (dyn traits): composition of minimal interfaces instead of deep inheritance trees. I know OOP does not fit well with modern languages and approaches, but the existing codebase and the habits of the teams since the 90s are still so present that the students must be at least aware of this paradigm (even if we don't encourage its usage for new developments).
In this playground is shown a minimal example inspired from an exercise formerly done in C++ (many other things exist around this excerpt).
At the abstract level, an Entity has an internal state (a position here, to keep it simple) and several dynamic components responsible for various behaviours (drawing, animation, reaction to events...).
These dynamic components implement some predefined interfaces (dyn traits) and can be freely defined at the application level (the abstract level does not have to know the details of these components).
Some of these components can have some internal data which could even be mutated.
For example, in this minimal code, a Shape component if mainly dedicated to drawing (no mutable operation is required for the entity or this component in general), but an Animator component can cause mutations on the entity (let's say its position), on the component itself and even on other components (change the color of the next drawing for example).
As requested in a comment, here is the code inline:
mod common {
pub trait Shape {
fn draw(
&self,
entity: &Entity,
);
fn change_color(
&mut self,
color: String,
);
}
pub trait Animator {
fn animate(
&mut self,
entity: &mut Entity,
);
}
#[derive(Debug)]
pub struct Pos {
pub x: f64,
pub y: f64,
}
pub struct Entity {
pos: Pos,
shape: Box<dyn Shape>,
animator: Box<dyn Animator>,
}
impl Entity {
pub fn new(
pos: Pos,
shape: Box<dyn Shape>,
animator: Box<dyn Animator>,
) -> Self {
Self {
pos,
shape,
animator,
}
}
pub fn pos(&self) -> &Pos {
&self.pos
}
pub fn pos_mut(&mut self) -> &mut Pos {
&mut self.pos
}
pub fn change_color(
&mut self,
color: String,
) {
self.shape.change_color(color);
}
pub fn draw(&self) {
self.shape.draw(self);
}
pub fn animate(&mut self) {
let anim = &mut self.animator;
anim.animate(self);
}
}
}
mod custom {
use super::common::{Animator, Entity, Shape};
pub struct MyShape {
color: String,
}
impl MyShape {
pub fn new(color: String) -> Self {
Self { color }
}
}
impl Shape for MyShape {
fn draw(
&self,
entity: &Entity,
) {
println!("draw at {:?} with {:?}", entity.pos(), self.color);
}
fn change_color(
&mut self,
color: String,
) {
self.color = color;
}
}
pub struct MyAnim {
count: i32,
}
impl MyAnim {
pub fn new() -> Self {
Self { count: 0 }
}
}
impl Animator for MyAnim {
fn animate(
&mut self,
entity: &mut Entity,
) {
let pos = entity.pos_mut();
if (self.count % 2) == 0 {
pos.x += 0.1;
pos.y += 0.2;
} else {
pos.x += 0.2;
pos.y += 0.1;
}
self.count += 1;
if self.count >= 3 {
entity.change_color("red".to_owned());
}
}
}
}
fn main() {
use common::{Entity, Pos};
use custom::{MyAnim, MyShape};
let mut entity = Entity::new(
Pos { x: 0.0, y: 0.0 },
Box::new(MyShape::new("green".to_owned())),
Box::new(MyAnim::new()),
);
entity.draw();
for _ in 0..5 {
entity.animate();
entity.draw();
}
}
As you can see, the provided code cannot be compiled since, at line 66, anim is a mutable reference to the Animator component responsible for the dynamic dispatch but the parameter of the method is also a mutable reference to the Entity as a whole which contains the previous Animator.
This parameter is needed if we want the Animator to be able to make changes on the entity.
I'm stuck with this situation and I can only think about workarounds that look quite ugly to me:
- don't pass the entity as a parameter but each of its field (except the animator) as many parameters: what's the point of defining structs then? (if an entity is made of twelve fields, should I pass eleven parameters every time I would act on this entity?)
- embed each field of an entity in a
RefCelland pretend every parameter of every function is a non-mutable reference, thenborrow_mut()everywhere we want to and hope it won't panic: for me, it's like giving-up the idea that function prototypes tell and enforce the intent of the code (let's add someRceverywhere in order to totally forget who owns what, and we obtain Java ;^)
I'm certain I did some bad choices about what deserves to be exclusive (&mut) or shared (&), but I can't see a reasonable limit.
In my opinion, when an entity has to be animated, it's its own concern: there is nothing to be shared, except looking at the state of the surrounding environment (but not changing it).
If we share everything and rely on interior-mutability in order to enable safe mutations at run-time (thanks to the ref-count) it sounds to me like: «let's drive like crazy, as if there were no traffic regulations, as long as no one complains (try_borrow()/try_borrow_mut()) and we don't have any accident (panic!())».
Could anyone suggest a better organisation of my structs/functions in order to enable the intended behaviour: an entity made of a few dynamic (as in OOP) components responsible for the details of the actions on the concerned entity?
Many months later... I answer my own question in case some remarks could be made about the solution I decided to use.
As a first attempt, and as kindly suggested by @cdhowie, I started by isolating the data members (only
poshere) ofEntityin anEntityStatestructure used as the only data-member ofEntity. This way, I could makeAnimator::animate()expectstate: &mut EntityStateinstead ofentity: &mut Entityas parameter; doing so, an implementation ofAnimatorwas able to mutate the position of anEntity. However, I was not fully satisfied because this led to a strict distinction between some members of anEntityonly because of the borrow-checker. For example, I could not invokeEntity::change_color()from anAnimatorbecause it implies theshapemember which is not inEntityState. Of course, we could decide to includeshapeinEntityStateas well, but what if we had another behavioural component (Interactor...) able to mutate theEntity(itsstate) and subject to mutations by other behavioural components (asAnimatorcould want to mutateShape)? I find it difficult to define a general rule in order to decide which members deserve to stand inEntityStateor just inEntity(and using interior-mutability for every single member looks cumbersome to me).By chance, while struggling with callback problems (which are quite similar to this problem, actually), I found this answer which uses a trick that I find brilliant and obvious once someone else than me has invented it! The behavioural member that needs
&mut selfwhen invoked is stored in anOption. It is simply taken from theOptionbefore the invocation and put back into afterwards: this way a&mut Entityparameter of this invocation cannot reach it any more via theEntityand the borrow-checker finds this situation correct. This solution only requires minimal changes in the original organisation of the code and, as far as I can foresee, it seems to keep being usable when the scenario gets more complex (more behavioural components, eventually interacting).Back to the example provided in the question, only three minor changes are needed (playground). In the structure, the member is wrapped into an
Option.Obviously, the construction of such a structure considers this
Option.The main point stands here: take the behavioural member from the
Option, invoke its function, providing a reference to the whole structure, put back the behavioural member into theOption.