Rust vs. State

Important : for a comfortable reading of the article, you need to be able to read the source code in Rust and understand why wrapping everything in Rc<RefCell<...>>is bad.



Introduction



Rust is not generally considered an object-oriented language: there is no implementation inheritance; at first glance, there is no encapsulation either; Finally, the dependency graphs of mutable objects so familiar to OOP-adepts look as ugly as possible (just look at all these Rc<RefCell<...>>and Arc<Mutex<...>>!)



True, implementation inheritance has been considered harmful for several years, and OOP gurus say very correct things like "a good object is an immutable object". So I wondered: How well do Object Thinking and Rust really fit together ?



The first guinea pig will be the State pattern, the pure implementation of which is the subject of this article.



It was chosen for a reason: a chapter from The Rust Book is devoted to the same pattern . The goal of that chapter was to show that only bad boys and girls write object-oriented code in Rust: here you Optionneed to copy and paste both unnecessary and trivial method implementations into all implementations of the trait. But if you apply a couple of tricks, the entire boilerplate will disappear, and readability will increase.



Scale of work



The original article modeled the workflow of a blog post. Let's show our imagination and adapt the original description to the harsh Russian realities:



  1. Any article on Habré was once an empty draft, which the author had to fill with content.
  2. When the article is ready, it is sent for moderation.
  3. As soon as the moderator approves the article, it is published on Habré.
  4. Until the article is published, users should not see its content.


Any illegal actions with the article should have no effect (for example, you cannot publish an unapproved article from the sandbox).



The listing below demonstrates the code corresponding to the description above.



// main.rs

use article::Article;

mod article;

fn main() {
    let mut article = Article::empty();

    article.add_text("Rust    -");
    assert_eq!(None, article.content());

    article.send_to_moderators();
    assert_eq!(None, article.content());

    article.publish();
    assert_eq!(Some("Rust    -"), article.content());
}


Article so far looks like this:



// article/mod.rs

pub struct Article;

impl Article {
    pub fn empty() -> Self {
        Self
    }

    pub fn add_text(&self, _text: &str) {
        // no-op
    }

    pub fn content(&self) -> Option<&str> {
        None
    }

    pub fn send_to_moderators(&self) {
        // no-op
    }

    pub fn publish(&self) {
        // no-op
    }
}


This goes through all asserts except the last one. Not bad!



Implementation of the pattern



Let's add an empty trait State, a state Draftand a couple of fields to Article:



// article/state.rs

pub trait State {
    // empty
}

// article/states.rs

use super::state::State;

pub struct Draft;

impl State for Draft {
    // nothing
}

// article/mod.rs

use state::State;
use states::Draft;

mod state;
mod states;

pub struct Article {
    state: Box<dyn State>,
    content: String,
}

impl Article {
    pub fn empty() -> Self {
        Self {
            state: Box::new(Draft),
            content: String::new(),
        }
    }

    // ...
}


Troubles with head off design



State, . , - :



trait State {
    fn send_to_moderators(&mut self) -> &dyn State;
}


, , , — .



?



pub trait State {
    fn send_to_moderators(&mut self) -> Box<dyn State>;
}


. . , ?



:



pub trait State {
    fn send_to_moderators(self: Box<Self>) -> Box<dyn State>;
}


: ( self). , Self: Sized, .. . trait object, .. .





: , , , . , , ; , .



P.S.: Amethyst.



use crate::article::Article;

pub trait State {
    fn send_to_moderators(&mut self) -> Transit {
        Transit(None)
    }
}

pub struct Transit(pub Option<Box<dyn State>>);

impl Transit {
    pub fn to(state: impl State + 'static) -> Self {
        Self(Some(Box::new(state)))
    }

    pub fn apply(self, article: &mut Article) -> Option<()> {
        article.state = self.0?;
        Some(())
    }
}


, , Draft:



// article/states.rs

use super::state::{State, Transit};

pub struct Draft;

impl State for Draft {
    fn send_to_moderators(&mut self) -> Transit {
        Transit::to(PendingReview)
    }
}

pub struct PendingReview;

impl State for PendingReview {
    // nothing
}

// article/mod.rs

impl Article {
    // ...
    pub fn send_to_moderators(&mut self) {
        self.state.send_to_moderators().apply(self);
    }
    // ...
}


-



: Published, State, publish PendingReview. Article::publish :)



. content State, Published , , Article:



// article/mod.rs

impl Article {
    // ...
    pub fn content(&self) -> Option<&str> {
        self.state.content(self)
    }
    // ...
}

// article/state.rs

pub trait State {
    // ...
    fn content<'a>(&self, _article: &'a Article) -> Option<&'a str> {
        None
    }
}

// article/states.rs

impl State for Published {
    fn content<'a>(&self, article: &'a Article) -> Option<&'a str> {
        Some(&article.content)
    }
}


, ? , !



impl Article {
    // ...
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
    // ...
}


( ) , .



! !



, Article , - , , . ? , ! .





.



, - Rust , , . - -.



, , Rust . , Observer: , Arc<Mutex<...>>!



, .




All Articles