Giao diện
Domain-Driven Design in Rust Type-Safe
"Make Illegal States Unrepresentable" — Dùng Type System để enforce business rules
Triết lý DDD trong Rust
Rust có Type System mạnh nhất trong các ngôn ngữ mainstream. Thay vì validate data tại runtime (như Java/Python), ta encode business rules trực tiếp vào types.
┌─────────────────────────────────────────────────────────────────────┐
│ RUNTIME VALIDATION vs COMPILE-TIME TYPES │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Traditional (Java/Python): │
│ ┌─────────────────────────────────────────────────────────────────┐│
│ │ String email = input; ││
│ │ if (!isValidEmail(email)) throw new Error(); // RUNTIME ││
│ │ // Compiler không biết email đã validated ││
│ └─────────────────────────────────────────────────────────────────┘│
│ │
│ Rust DDD: │
│ ┌─────────────────────────────────────────────────────────────────┐│
│ │ let email: Email = Email::parse(input)?; // COMPILE-TIME ││
│ │ // Nếu có Email, CHẮC CHẮN nó valid ││
│ │ // Compiler BIẾT và ENFORCE ││
│ └─────────────────────────────────────────────────────────────────┘│
│ │
└─────────────────────────────────────────────────────────────────────┘1. Newtype Pattern for Validation
Newtype = Wrapper type đảm bảo data đã được validate.
Ví dụ: Email Value Object
rust
use std::fmt;
/// Email đã được validate - Không thể tạo Email invalid
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Email(String); // Private inner field!
impl Email {
/// Parse và validate email. Trả về Err nếu invalid.
pub fn parse(s: &str) -> Result<Self, EmailError> {
// RFC 5322 simplified validation
if s.contains('@') && s.len() >= 5 {
Ok(Email(s.to_lowercase()))
} else {
Err(EmailError::InvalidFormat)
}
}
/// Borrow inner string (read-only)
pub fn as_str(&self) -> &str {
&self.0
}
}
// Không implement From<String> để buộc đi qua parse()!
#[derive(Debug, thiserror::Error)]
pub enum EmailError {
#[error("Invalid email format")]
InvalidFormat,
}Sử dụng
rust
fn register_user(email_input: String) -> Result<User, Error> {
// ✅ Buộc validate tại boundary
let email = Email::parse(&email_input)?;
// Từ đây trở đi, email CHẮC CHẮN valid
// Không cần validate lại ở DB layer, notification layer, etc.
let user = User::new(email, ...);
Ok(user)
}Thêm Newtypes phổ biến
rust
/// Non-empty string (tên người dùng, title, etc.)
#[derive(Debug, Clone)]
pub struct NonEmptyString(String);
impl NonEmptyString {
pub fn new(s: String) -> Result<Self, ValidationError> {
if s.trim().is_empty() {
Err(ValidationError::Empty)
} else {
Ok(Self(s))
}
}
}
/// Positive integer (quantity, age, etc.)
#[derive(Debug, Clone, Copy)]
pub struct PositiveInt(u32);
impl PositiveInt {
pub fn new(n: u32) -> Result<Self, ValidationError> {
if n == 0 {
Err(ValidationError::Zero)
} else {
Ok(Self(n))
}
}
}
/// Money amount (cents để tránh floating point)
#[derive(Debug, Clone, Copy)]
pub struct Money {
cents: i64,
currency: Currency,
}2. Type State Pattern
Encode trạng thái của object vào type system. Compiler ngăn chặn invalid transitions.
Ví dụ: Order Lifecycle
rust
// Marker types (Zero-sized)
pub struct Draft;
pub struct Submitted;
pub struct Paid;
pub struct Shipped;
pub struct Cancelled;
// Order generic over State
pub struct Order<State> {
id: OrderId,
items: Vec<OrderItem>,
total: Money,
_state: std::marker::PhantomData<State>,
}
impl Order<Draft> {
pub fn new() -> Self {
Order {
id: OrderId::new(),
items: vec![],
total: Money::zero(),
_state: PhantomData,
}
}
pub fn add_item(&mut self, item: OrderItem) {
self.items.push(item);
self.total = self.total + item.price;
}
// Transition: Draft → Submitted
pub fn submit(self) -> Result<Order<Submitted>, OrderError> {
if self.items.is_empty() {
return Err(OrderError::EmptyOrder);
}
Ok(Order {
id: self.id,
items: self.items,
total: self.total,
_state: PhantomData,
})
}
}
impl Order<Submitted> {
// Transition: Submitted → Paid
pub fn mark_paid(self, payment: Payment) -> Order<Paid> {
Order {
id: self.id,
items: self.items,
total: self.total,
_state: PhantomData,
}
}
// Transition: Submitted → Cancelled
pub fn cancel(self, reason: String) -> Order<Cancelled> {
// ...
}
}
impl Order<Paid> {
// Transition: Paid → Shipped
pub fn ship(self, tracking: TrackingNumber) -> Order<Shipped> {
// ...
}
}Usage: Compiler Enforces Valid Transitions
rust
fn process_order() {
let order = Order::<Draft>::new();
order.add_item(item1);
let submitted = order.submit()?;
// ❌ COMPILE ERROR: Order<Submitted> không có method add_item
// submitted.add_item(item2);
// ❌ COMPILE ERROR: Order<Submitted> không có method ship
// submitted.ship(tracking);
// ✅ OK: Valid transition
let paid = submitted.mark_paid(payment);
let shipped = paid.ship(tracking);
}3. Algebraic Data Types cho Business Logic
Dùng enum để model exclusive states.
Ví dụ: Payment Status
rust
pub enum PaymentStatus {
Pending,
Processing {
started_at: DateTime<Utc>,
gateway_id: String,
},
Completed {
completed_at: DateTime<Utc>,
transaction_id: TransactionId,
},
Failed {
failed_at: DateTime<Utc>,
reason: PaymentFailureReason,
retries: u8,
},
Refunded {
refunded_at: DateTime<Utc>,
amount: Money,
},
}
pub enum PaymentFailureReason {
InsufficientFunds,
CardDeclined,
NetworkError,
FraudDetected,
}Pattern Matching: Exhaustive Handling
rust
fn handle_payment(status: PaymentStatus) -> Action {
match status {
PaymentStatus::Pending => Action::WaitForGateway,
PaymentStatus::Processing { started_at, .. } => {
if Utc::now() - started_at > Duration::minutes(5) {
Action::Timeout
} else {
Action::Wait
}
}
PaymentStatus::Completed { transaction_id, .. } => {
Action::NotifySuccess(transaction_id)
}
PaymentStatus::Failed { reason, retries, .. } => {
if retries < 3 && is_retryable(&reason) {
Action::Retry
} else {
Action::NotifyFailure(reason)
}
}
PaymentStatus::Refunded { amount, .. } => {
Action::NotifyRefund(amount)
}
// ❌ COMPILE ERROR nếu thiếu case!
}
}4. Domain Events
Events immutable, được type hóa đầy đủ.
rust
#[derive(Debug, Clone)]
pub enum DomainEvent {
UserRegistered {
user_id: UserId,
email: Email,
registered_at: DateTime<Utc>,
},
OrderPlaced {
order_id: OrderId,
user_id: UserId,
total: Money,
},
PaymentReceived {
order_id: OrderId,
payment_id: PaymentId,
amount: Money,
},
}
// Event handler type-safe
pub trait EventHandler {
fn handle(&self, event: DomainEvent) -> Result<(), EventError>;
}5. Bảng Tóm tắt DDD Patterns
| Pattern | Rust Implementation | Benefit |
|---|---|---|
| Value Object | Newtype (struct Email(String)) | Validation at construction |
| Entity | Struct with ID | Identity-based equality |
| Aggregate | Struct containing entities | Consistency boundary |
| Type State | Generics with marker types | Compile-time state machine |
| Domain Event | Enum variants | Exhaustive event handling |
| Repository | Trait (Port) | Infrastructure abstraction |
Anti-Patterns to Avoid
❌ Primitive Obsession
rust
// BAD: String có thể chứa gì cũng được
fn create_user(email: String, name: String, age: i32) { ... }
// GOOD: Types encode meaning
fn create_user(email: Email, name: NonEmptyString, age: PositiveInt) { ... }❌ Leaky Abstraction
rust
// BAD: Domain biết về database
pub struct User {
pub id: i64, // Database auto-increment leaking
}
// GOOD: Domain-specific ID
pub struct User {
pub id: UserId, // Opaque, có thể là UUID, ULID, etc.
}