If you have written Rust code, you may have heard of so many great things about type state pattern
. It’s where the current state of an object is kept in check whereby you can expose only the interfaces allowed given the state the object currently is in.
Ok, the idea is good but I was wondering then how you can persist them on the database especially when you use ORM? Well, it turns out it was not as easy as you might think it is: (Reference: https://www.reddit.com/r/rust/comments/m7nox4/comment/grf0sle/?utm_source=reddit&utm_medium=web2x&context=3)
But let’s make it clear that type state pattern
itself was not meant for OOP. OOP style state pattern
is basically where you use one field with values that are yet another struct (or class) that you delegate the tasks to so you can encapsulate things.
In this article, I just tinkered with possible workaround but if you have any suggestion to make an improvement, it will be more than welcome.
[dependencies]
serde = { version="*" , features= ["derive"] }
serde_json = "*"
use serde::{Deserialize, Serialize};
pub struct Transaction{
items: Vec<String>,
amount: i32,
}
#[derive(Serialize,Deserialize,Debug)]
struct InCart{
items: Vec<String>,
amount: i32,
}
impl InCart{
pub fn request_purchase(self) -> PurchaseRequested{
PurchaseRequested { items: self.items
, amount: self.amount }
}
}
#[derive(Serialize,Deserialize,Debug)]
struct PurchaseRequested{
items: Vec<String>,
amount: i32,
}
impl PurchaseRequested{
pub fn make_purchase(self)->PurchaseMade{
PurchaseMade { items: self.items, amount: self.amount }
}
}
#[derive(Serialize,Deserialize,Debug)]
struct PurchaseMade{
items: Vec<String>,
amount: i32,
}
Ok, this is basic implementation of typestate pattern
which keeps track of transaction states. What it enables is:
fn main(){
let transaction = Transaction::new(
vec!["item1".to_string(),"item2".to_string(),"item3".to_string()],
30000
); // returns 'InCart' object
let transaction = transaction.request_purchase(); // returns 'PurchaseRequested' object
let transaction = transaction.make_purchase(); // returns 'PurchaseMade' object
The benefit of this implementation is say, when the transaction is in InCart
state, you can’t invoke, for example, make_purchase
method before calling request_purchase
. (I’m just showing an example. Be aware that business requirements vary).
The problem is, you don’t usually expect that you get just one request from a client, and the business transaction ends. Each state should be stored in the database through database transactions. Then how do we do?
My idea on that was to have enum which can act as thin wrapper around each state struct we defined. Let’s see public interface first:
fn main(){
let transaction = Transaction::new(
vec!["item1".to_string(),"item2".to_string(),"item3".to_string()],
30000
);
let transaction = transaction.request_purchase();
let transaction = transaction.make_purchase();
let trx_state = transaction.state(); // By calling state(), it returns current struct state wrapped in enum.
Let’s see enum implementation:
#[derive(Serialize,Deserialize,Debug)]
#[serde(tag = "type")]
enum OrderState<T:TransactionState>{
InCart(Box<T>),
PurchaseRequested(Box<T>),
PurchaseMade(Box<T>)
}
Okay, so each field name is equivalent to that of the state struct. Note that this enum is generic one for the type that implements TransactionState
. (Yes, I suck at naming…)
And the TransactionState
enforces one method to be implemented:
trait TransactionState {
fn state(self)->OrderState<Self> where Self:std::marker::Sized;
}
And then, your state structs implement this trait as follows:
impl TransactionState for InCart{
fn state(self)->OrderState<Self> {
OrderState::InCart(Box::new(self))
}
}
impl TransactionState for PurchaseRequested{
fn state(self)->OrderState<Self> {
OrderState::PurchaseRequested(Box::new(self))
}
}
impl TransactionState for PurchaseMade{
fn state(self)->OrderState<Self> {
OrderState::PurchaseMade(Box::new(self))
}
}
So what it does is just convert the state struct into being wrapped in enum, OrderState
.
And then, to convert enum back to state struct, the following is implemented
impl<T:TransactionState> OrderState<T>{
fn state(self)->Box<T>{
match self{
OrderState::InCart(strt)=>strt,
OrderState::PurchaseRequested(strt)=>strt,
OrderState::PurchaseMade(strt)=>strt
}
}
}
So what have we gotten?
fn main(){
let transaction = Transaction::new(
vec!["item1".to_string(),"item2".to_string(),"item3".to_string()],
30000
);
let transaction = transaction.request_purchase();
let transaction = transaction.make_purchase();
let trx_state = transaction.state();
println!("{:?}",serde_json::json!(&trx_state)); // serializable and it contains type information!
let transaction:Box<PurchaseMade> = trx_state.state(); //it gets back to state struct you can operate on.
println!("{:?}",&transaction);
}
Result on console:
Object {"amount": Number(30000), "items": Array [String("item1"), String("item2"), String("item3")], "type": String("PurchaseMade")}
PurchaseMade { items: ["item1", "item2", "item3"], amount: 30000 }