Extending the state pattern so it is known when "invalid" state transitions are taken

I'm designing a system that will heavely rely on the state object pattern.

One feature we want of the system is to know when invalid or undefined state transitions are taken. The state pattern as implemented in the The Rust Programming Language silently puts the old state back into the object and there is no indication an undefined transition was taken.

To start with, I changed the code so that the state transitions return Result but this becomes tricky in the calling code because the object's state is take'n out and has to be replaced with the old state, but ownership of the state is taken by the trait methods. To work around that I return ownership of the old state back through the Err variant.

I find that to be pretty ugly and can't help but wonder if there's a better way.

The end of the chapter in the book describes implementing the state pattern using structs for each state. The only problem I have with that is our state objects will be stored in a database, and when retrieved you never know which state it will be in. I can see a lot of duplicated checks made to coerce the record into a specific state type.. and then more checks if an action can be taken on that type. A single type would be a lot easier to work with.

So anyways I'm just looking for some guidance on this.

pub struct Foo {
    state: Option<Box<dyn State>>,
    bar: i32,
}

impl Foo {
    fn bar(&mut self) -> Result<(), String> {
        if let Some(state) = self.state.take() {
            match state.bar(self) {
                Ok(state) => {
                    self.state = Some(state);
                    Ok(())
                },
                Err((state, error)) => {
                    self.state = Some(state);
                    Err(error)
                }
            }
        } else {
            panic!("No state");
        }
    }
}

trait State {
    fn bar(
        self: Box<Self>,
        foo: &mut Foo,
    ) -> Result<Box<dyn State>, (Box<dyn State>, String)>;
}

struct StateA {}

impl State for StateA {
    fn bar(self: Box<Self>, foo: &mut Foo) -> Result<Box<dyn State>, (Box<dyn State>, String)> {
        foo.bar = 42;
        Ok(Box::new(StateB {}))
    }

}

struct StateB {}

impl State for StateB {
    fn bar(self: Box<Self>, foo: &mut Foo) -> Result<Box<dyn State>, (Box<dyn State>, String)> {
        Err((self, "Transition not allowed".to_string()))
    }
}

Only a partial answer, sorry, but returning the current value in the error has precedent with Sender in std::sync::mpsc - Rust

1 Like

One option would be to have a more complicated error type that holds the original.


Because your trait methods take a &mut Foo, you could put the onus on them to restore the state.

You can get rid of the Option if you have a default State implementation to use fall back to (e.g. given a poor State implementor).

Here's an example of those two approaches.

That said, I don't know that I've really seen this pattern elsewhere; the erased nature of what state you're in is a bit odd. I believe the type-per-state pattern is more common.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.