This post is about an example implementation of the State Design Pattern in Rust.
State Design Pattern
Anything has a state. An entity has a state. Similarly, a car can be in a state of acceleration. While it is accelerating, there are some things it certainly cannot or should not do, e.g., reversing at that very moment. The State Design Pattern treats each state as a struct whose functions represent movements to the other states. Each struct can allow or prevent some status changes. Generally, these functions run first before the actual status change that happens internally.
In technical terms, please read State Pattern.
Our Implementation
Our implementation is about a task ticket that has to be completed. Now, consider this like a JIRA ticket but only way simpler, which can it can be in any of the following states at one time: New, In Progress, or Done.
NEW means that a ticket is newly created while IN PROGRESS means that someone has started working on it. DONE means it is resolved.
Rules for Changing States
The rules of status change are simple, as depicted in the following image.
A ticket cannot go through the following status changes.
- New tickets cannot change the status to NEW or DONE.
- In-progress tickets cannot change status NEW or IN PROGRESS.
- Processed tickets cannot change the status to NEW, DONE, or IN PROGRESS
Rust Codes
We have a struct JiraTicket that represents a JIRA ticket. It implements the trait Debug, State, and JiraTicketStateMovement.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | struct JiraTicket { ticket_id: String, current_status: Box<JiraTicketStateMovement> } impl Debug for JiraTicket { fn fmt(&self, f: &mut Formatter<'_>) -> Result { write!(f, "##### Ticket {} is at {}", self.ticket_id, self.current_status.get_state()) } } impl JiraTicket { fn new(p_ticket_id: String) -> Box<JiraTicket> { Box::new(JiraTicket { ticket_id: p_ticket_id, current_status: Box::new( NewState { state_name: "NEW".to_string() } ) }) } } impl State for JiraTicket { fn get_state(&self) -> String { unimplemented!() } } impl JiraTicketStateMovement for JiraTicket { fn to_new(&mut self) -> bool { println!(" + {} -> {}", self.current_status.get_state(), "NEW"); let move_ok = self.current_status.to_new(); if move_ok { self.current_status = Box::new(InProgressState { state_name: "NEW".to_string() }); } move_ok } fn to_in_progress(&mut self) -> bool { println!(" + {} -> {}", self.current_status.get_state(), "IN PROGRESS"); let move_ok = self.current_status.to_in_progress(); if move_ok { self.current_status = Box::new(InProgressState { state_name: "IN PROGRESS".to_string() }); } move_ok } fn to_done(&mut self) -> bool { println!(" + {} -> {}", self.current_status.get_state(), "DONE"); let move_ok = self.current_status.to_done(); if move_ok { self.current_status = Box::new(InProgressState { state_name: "DONE".to_string() }); } move_ok } } |
Traits and State Codes
Both the JiraTicket and the “state” structs use these traits.
1 2 3 4 5 6 7 8 | trait State { fn get_state(&self) -> String; } trait JiraTicketStateMovement: State { fn to_new(&mut self) -> bool; fn to_in_progress(&mut self) -> bool; fn to_done(&mut self) -> bool; } |
NEW State
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | struct NewState { state_name: String } impl State for NewState { fn get_state(&self) -> String{ self.state_name.clone() } } impl JiraTicketStateMovement for NewState { fn to_new(&mut self) -> bool { println!(" * You are already in the NEW status"); false } fn to_in_progress(&mut self) -> bool { true } fn to_done(&mut self) -> bool { println!(" * Cannot move to DONE status"); false } } |
IN PROGRESS State
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | struct InProgressState { state_name: String } impl State for InProgressState { fn get_state(&self) -> String{ self.state_name.clone() } } impl JiraTicketStateMovement for InProgressState { fn to_new(&mut self) -> bool { println!(" * Cannot move to NEW status"); false } fn to_in_progress(&mut self) -> bool { println!(" * You are already in the IN PROGRESS status"); false } fn to_done(&mut self)-> bool { true } } |
DONE State
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | struct Donestate { state_name: String } impl State for Donestate { fn get_state(&self) -> String{ self.state_name.clone() } } impl JiraTicketStateMovement for Donestate { fn to_new(&mut self) -> bool { println!(" * Cannot move to NEW status"); false } fn to_in_progress(&mut self) -> bool { println!(" * Cannot move to IN PROGRESS status"); false } fn to_done(&mut self)-> bool { println!(" * Cannot move to DONE status"); false } } |
Usage and Demo
The codes try to change the ticket’s status from IN PROGRESS to NEW.
1 2 3 4 5 6 7 8 9 10 11 | fn main() { let mut jira_ticket = JiraTicket::new("TUR-1".to_string()); println!("{:?}", jira_ticket); jira_ticket.to_in_progress(); println!("{:?}", jira_ticket); jira_ticket.to_new(); println!("{:?}", jira_ticket); jira_ticket.to_done(); println!("{:?}", jira_ticket); } |
Output:
1 2 3 4 5 6 7 8 | ##### Ticket TUR-1 is at NEW + NEW -> IN PROGRESS ##### Ticket TUR-1 is at IN PROGRESS + IN PROGRESS -> NEW * Cannot move to NEW status ##### Ticket TUR-1 is at IN PROGRESS + IN PROGRESS -> DONE ##### Ticket TUR-1 is at DONE |
What happens to our Rust State Design Pattern Example when we have a new state? With the State Design Pattern, we only need to modify JiraTicketStateMovement and the “state” structs. First, add a new function for the new state. Then, implement the function in the “state” structs in our Rust example.
Tested with Rust 1.39.0.