So sánh cách tiếp cận: Dynamic State vs. Typestate Pattern
Hầu hết các quy trình nghiệp vụ đều tuân theo các vòng đời (lifecycle) nghiêm ngặt. Một đơn hàng thương mại điện tử bắt đầu ở trạng thái Draft, chuyển sang Paid và kết thúc ở Shipped. Một network socket bắt đầu từ Disconnected và chuyển thành Connected. Khi mô hình hóa các vòng đời này, các lập trình viên thường chọn một trong hai hướng đi.
1. Tiếp cận truyền thống bằng cờ trạng thái tại Runtime
Trong nhiều codebase, trạng thái được lưu trữ dưới dạng một trường enum bên trong một struct nguyên khối, được kiểm soát bởi các bước kiểm tra lúc runtime:
enum OrderStatus {
Draft,
Paid,
Shipped,
}
struct Order {
id: u64,
status: OrderStatus,
items: Vec<String>,
}
impl Order {
fn ship(&mut self) -> Result<(), String> {
match self.status {
OrderStatus::Paid => {
self.status = OrderStatus::Shipped;
Ok(())
}
_ => Err("Không thể giao đơn hàng chưa thanh toán hoặc đã được giao".into()),
}
}
}
Điều gì xảy ra khi ai đó gọi ship() trên một đơn hàng nháp (draft)? Trình biên dịch không hề lên tiếng. Mã nguồn vẫn biên dịch bình thường, và lỗi này sẽ âm thầm lọt vào production cho đến khi khách hàng gặp phải lỗi 500 không mong muốn.
2. Typestate Pattern
Typestates biến các giai đoạn vòng đời động thành các kiểu dữ liệu riêng biệt ngay tại thời điểm biên dịch (compile time). Thay vì lưu một cờ enum có thể thay đổi (mutable), struct của bạn mang một marker type có kích thước bằng 0 đại diện cho giai đoạn hiện tại. Các phương thức chuyển đổi trạng thái sau đó sẽ tiêu thụ self theo giá trị (by value) và trả về một kiểu hoàn toàn mới:
// Chỉ Order<Paid> mới implement phương thức ship()
let paid_order: Order<Paid> = draft_order.pay();
let shipped_order: Order<Shipped> = paid_order.ship("DHL-8841");
Bây giờ hãy thử gọi ship() trên draft_order xem. Mã nguồn sẽ lập tức báo lỗi biên dịch. Trình biên dịch từ chối tạo file thực thi vì Order<Draft> hoàn toàn không có phương thức ship() nào trong scope.
Ưu và nhược điểm: Đánh giá Typestate cho Codebase của bạn
Chuyển dịch các ràng buộc bất biến từ kiểm tra runtime sang bảo đảm ở compile-time mang lại sự an tâm tuyệt đối. Tuy nhiên, cũng như bất kỳ design pattern nào, nó cũng đi kèm các đánh đổi kỹ thuật thực tế.
Ưu điểm
- Triệt tiêu các trạng thái không hợp lệ: Các quy tắc nghiệp vụ trở thành những ràng buộc vật lý của hệ thống kiểu dữ liệu. Bạn không thể vô tình trừ tiền một đơn hàng đã hủy hoặc xuất kho cho một hóa đơn chưa thanh toán.
- Không tốn chi phí Runtime (Zero Runtime Overhead): Trên hệ thống 64-bit,
std::mem::size_of::<Order<Draft>>()có kích thước chính xác là 32 bytes (8 bytes chou64ID cộng với 24 bytes choVec). Các kiểu dữ liệu kích thước bằng 0 (ZSTs) vàPhantomDatacủa Rust sẽ biến mất hoàn toàn trong quá trình tối ưu hóa của LLVM. Bạn đạt được mức độ an toàn tuyệt đối mà không tiêu tốn thêm chu kỳ CPU hay phải cấp phát bộ nhớ heap. - Cơ chế Ownership ngăn chặn các tham chiếu lỗi thời: Vì các hàm chuyển đổi nhận
selftheo giá trị (by value), Rust sẽ tiêu thụ trạng thái trước đó. Việc gọi phương thức trên trạng thái cũ sẽ kích hoạt lỗi biên dịchE0382: use of moved value. - Gợi ý code (Autocomplete) trong IDE chính xác hơn: Kỹ sư chỉ nhìn thấy các phương thức hợp lệ cho trạng thái hiện tại của đối tượng. Điều này giúp giảm độ trễ khi làm quen dự án và loại bỏ cấu hình sai ngay trước khâu code review.
Đánh đổi
- Nhiều mã mẫu (Boilerplate): Việc định nghĩa marker traits, generic structs và các implementation riêng cho từng trạng thái đòi hỏi lượng code ban đầu nhiều hơn so với việc chỉ thêm một variant vào enum.
- Ranh giới lưu trữ dữ liệu (Persistence): Cơ sở dữ liệu quan hệ lưu các cột trạng thái dưới dạng chuỗi hoặc số nguyên phẳng (ví dụ:
status = "PAID"). Bạn phải viết một factory hoặc parser rõ ràng tại ranh giới dữ liệu để chuyển đổi các hàng đó thành các typestate cụ thể. - Tập hợp không đồng nhất (Heterogeneous Collections): Bạn không thể lưu một
Order<Draft>và mộtOrder<Paid>chung trong mộtVec<Order<S>>tiêu chuẩn. Bạn phải bọc chúng trong một enum tổng hoặc dùng dynamic trait objects.
Kiến trúc: Ba khối xây dựng cốt lõi
Để triển khai typestate chuẩn idiomatic, sẵn sàng cho môi trường production trong Rust, hãy dựa trên ba nguyên tắc nền tảng:
- Structs có kích thước bằng 0 (ZSTs): Định nghĩa các marker struct rỗng như
pub struct Draft;chiếm 0 byte trong bộ nhớ. - Sealed Marker Trait: Ràng buộc các trạng thái vào một trait được bảo vệ bởi private module. Điều này ngăn các crate bên ngoài tự ý gắn các trạng thái tùy tiện vào domain pipeline của bạn.
- Chuyển đổi tiêu thụ giá trị (
self): Luôn chiếm quyền sở hữu (ownership) củaselfthay vì mượn qua&mut self. Việc này đảm bảo trạng thái cũ bị hủy khi trạng thái tiếp theo bắt đầu.
Chúng tôi đã triển khai mô hình này trên một dịch vụ thanh toán xử lý khoảng 40.000 sự kiện checkout mỗi ngày. Trong suốt hai năm chạy production, chúng tôi không ghi nhận bất kỳ lỗi hồi quy nào liên quan đến chuyển đổi trạng thái không hợp lệ trong các pipeline quan trọng đó.
Hướng dẫn triển khai: Xây dựng Pipeline xử lý đơn hàng tin cậy
Dưới đây là từng bước xây dựng quy trình xử lý đơn hàng chuẩn chỉnh.
Bước 1: Định nghĩa các Marker Types và một Sealed Trait
Bắt đầu bằng việc tạo các kiểu rỗng cho từng trạng thái. Sử dụng pattern sealed trait để mã nguồn bên ngoài không thể tự tiện implement các trạng thái khác:
use std::marker::PhantomData;
// Ngăn các crate bên ngoài implement trait này
mod private {
pub trait Sealed {}
}
pub struct Draft;
pub struct Paid;
pub struct Shipped;
impl private::Sealed for Draft {}
impl private::Sealed for Paid {}
impl private::Sealed for Shipped {}
pub trait OrderState: private::Sealed {}
impl OrderState for Draft {}
impl OrderState for Paid {}
impl OrderState for Shipped {}
// Struct cốt lõi được tham số hóa bởi trạng thái S
pub struct Order<S: OrderState> {
pub id: u64,
pub items: Vec<String>,
_state: PhantomData<S>,
}
Bước 2: Triển khai các hành vi dùng chung
Triển khai các thuộc tính chung cho mọi trạng thái—chẳng hạn như đọc ID đơn hàng hoặc kiểm tra danh sách mặt hàng—sử dụng khối generic không bị ràng buộc:
impl<S: OrderState> Order<S> {
pub fn id(&self) -> u64 {
self.id
}
pub fn items(&self) -> &[String] {
&self.items
}
}
Bước 3: Triển khai các phương thức và chuyển đổi trạng thái cụ thể
Bây giờ hãy giới hạn các thao tác cho từng trạng thái riêng biệt. Chú ý cách mỗi lần chuyển đổi đều tiêu thụ self để chuyển giao quyền sở hữu gọn gàng:
impl Order<Draft> {
pub fn new(id: u64) -> Self {
Self {
id,
items: Vec::new(),
_state: PhantomData,
}
}
pub fn add_item(&mut self, item: String) {
self.items.push(item);
}
// Chuyển đổi: Draft -> Paid
pub fn pay(self) -> Order<Paid> {
Order {
id: self.id,
items: self.items,
_state: PhantomData,
}
}
}
impl Order<Paid> {
// Chuyển đổi: Paid -> Shipped
pub fn ship(self, tracking_number: &str) -> (Order<Shipped>, String) {
let receipt = format!("Đơn hàng {} đã được gửi qua {}", self.id, tracking_number);
let shipped = Order {
id: self.id,
items: self.items,
_state: PhantomData,
};
(shipped, receipt)
}
}
impl Order<Shipped> {
pub fn tracking_status(&self) -> &'static str {
"Đang vận chuyển cùng đơn vị giao vận"
}
}
Bước 4: Kiểm thử thực tế
Hãy xem cách trình biên dịch thực thi pipeline này trong thực tế:
fn main() {
let mut draft = Order::new(101);
draft.add_item("Bàn phím công thái học".to_string());
draft.add_item("Cáp USB-C".to_string());
// Chuyển đổi hợp lệ: Draft tự tiêu thụ và trả về Order<Paid>
let paid = draft.pay();
// LỖI BIÊN DỊCH [E0382]: sử dụng giá trị đã bị di chuyển: `draft`
// draft.add_item("Thảm trải bàn".to_string());
// LỖI BIÊN DỊCH: không tìm thấy phương thức `ship` cho kiểu `Order<Draft>`
// draft.ship("DHL-9921");
let (shipped, receipt) = paid.ship("DHL-9921");
println!("{}", receipt);
println!("Trạng thái hiện tại: {}", shipped.tracking_status());
}
Xử lý tầng lưu trữ dữ liệu thực tế (Persistence)
Khi tải các bản ghi từ PostgreSQL hoặc SQLite, hãy bọc các typestate trong một enum domain bao ngoài ngay tại ranh giới repository của bạn:
pub enum AnyOrder {
Draft(Order<Draft>),
Paid(Order<Paid>),
Shipped(Order<Shipped>),
}
impl AnyOrder {
pub fn from_db_record(id: u64, status: &str, items: Vec<String>) -> Result<Self, String> {
match status {
"DRAFT" => Ok(AnyOrder::Draft(Order { id, items, _state: PhantomData })),
"PAID" => Ok(AnyOrder::Paid(Order { id, items, _state: PhantomData })),
"SHIPPED" => Ok(AnyOrder::Shipped(Order { id, items, _state: PhantomData })),
other => Err(format!("Không nhận diện được trạng thái trong cơ sở dữ liệu: {}", other)),
}
}
}
Hãy cách ly logic kiểm tra dữ liệu ở ngay ranh giới I/O. Một khi dữ liệu đã đi vào lõi domain, typestates đảm bảo rằng những chuyển đổi trạng thái không hợp lệ sẽ không thể nào được biên dịch.

