'OOP in Rust and shared/mutable references
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?
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
