In the last article(https://velog.io/@migorithm/Fluent-Rust-Type-State-Pattern), I covered one way of handling type state pattern
in Rust.
As safe as it is, it has some drawbacks; multiple structs to describe potentially one DB entity, Box
smart pointer, enum
state manager and so on. Can we pare it down more? And well, Yes you can:
// states
#[derive(Deserialize,Serialize,Debug)]
pub struct InCart;
#[derive(Deserialize,Serialize,Debug)]
pub struct PurchaseRequested;
#[derive(Deserialize,Serialize,Debug)]
pub struct PurchaseMade;
// struct
pub struct Transaction<State=InCart>{
items: Vec<String>,
amount: i32,
state : String,
#[serde(skip_serializing)]
_state: PhantomData<State>
}
Surely, you wonder, “what’s with PhantomData
? Well, PhantomData
is zero-sized type that literally doesn’t take up any memory but just to mark things so the struct can “act like” it owns the type.
Let’s see the magic you can spell out with this:
impl Transaction<InCart>{
pub fn request_purchase(self) -> Transaction<PurchaseRequested>{
Transaction{_state : PhantomData::<PurchaseRequested>, amount: self.amount, items:self.items,state:"PurchaseRequested".to_string()}
}
}
impl Transaction<PurchaseRequested>{
pub fn make_purchase(self)->Transaction<PurchaseMade>{
Transaction { items: self.items, amount: self.amount, _state: PhantomData::<PurchaseMade> ,state:"PurchaseMade".to_string()}
}
}
impl Transaction{
fn new(items:Vec<String>,amount:i32)-> Transaction<InCart>{
Transaction { items, amount, _state: PhantomData::<InCart>,state:"InCart".to_string() }
}
}
You may have noticed that every time I implement methods, I specify the state in which the following methods can be invoked. That way, you can achieve the same effect as you have by implementing multiple structs that denote each state while cutting down the code to one struct.
Note also too that I manage two states, one(state
) is String
type state that is mainly for persistence and the other _state
is PhantomData
that’s for application side operation.
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();
println!("{:?}",serde_json::json!(&transaction)); // serializable and it contains type information!
}
// Object {"amount": Number(30000), "items": Array [String("item1"), String("item2"), String("item3")], "state": String("PurchaseMade")}