State Pattern trong C#: Vòng Đời Order với Stateless
State pattern trong C# / .NET 10: mô hình vòng đời order thành state với transition cho phép, dùng pattern matching hoặc thư viện Stateless.
Mục lục
- State pattern giải quyết bài toán gì trong C#?
- Hierarchy State giáo khoa trông thế nào?
- Khi nào enum cộng pattern matching thay State?
- Thư viện Stateless làm State machine dễ chịu thế nào?
- Khi nào State pattern bắn trật?
- So sánh State với Strategy và Memento thế nào?
- Một ví dụ thật trong .NET 10 trông thế nào?
- Đọc tiếp gì trong series?
Vòng đời order: Created khi cart checkout, Paid khi charge
thành công, Shipped khi warehouse book courier, Delivered
khi courier xác nhận giao. Cancelled có thể chen vào chuỗi
bất cứ lúc nào tới Shipped. Mỗi transition có rule: bạn không
ship được order chưa trả tiền, không charge được order đã
cancel, và Delivered là cuối.
Phiên bản đầu của Order.MarkPaid() là một method với if (_status == OrderStatus.Cancelled) throw ...; if (_status == OrderStatus.Paid) throw ...; .... Đến khi có mười method và
sáu transition, rule rải khắp class và thêm Refunded đòi đụng
mọi method.
State pattern là câu trả lời. Hành vi order phụ thuộc state;
mỗi state biết transition nào cho phép và làm gì khi một cái
fire. Trong C# hiện đại hierarchy class giáo khoa thường được
thay bởi enum cộng
Stateless,
nhưng pattern là ý tưởng cơ bản cả hai implementation giữ lại.
State pattern giải quyết bài toán gì trong C#?
Pattern xứng chỗ khi operation cho phép của object phụ thuộc giai đoạn lifecycle nó đang ở. Ba hình dạng cụ thể:
- Vòng đời domain. Order, account, document approval — tập hữu hạn state với rule ai được di chuyển tới đâu.
- Engine workflow. Ticket, lead, deployment. Mỗi bước có guard: "test phải pass trước khi bước deploy enable".
- Toggle mode UI. Chế độ edit, view, locked. Cùng button nhìn thấy nhưng tác dụng phụ thuộc mode.
Cái không phải bài toán State: "Tôi muốn đổi thuật toán" — đó là Strategy. "Tôi muốn ghi mỗi transition cho replay" — đi cặp với Command. State đặc biệt nói về hành vi điều khiển bởi giai đoạn lifecycle.
Hierarchy State giáo khoa trông thế nào?
Một interface chung, một class mỗi state, order ủy thác cho state hiện tại:
public interface IOrderState
{
IOrderState MarkPaid();
IOrderState Ship();
IOrderState Cancel();
string Name { get; }
}
public sealed class Created : IOrderState
{
public string Name => nameof(Created);
public IOrderState MarkPaid() => new Paid();
public IOrderState Ship() => throw new InvalidOperationException("Cannot ship unpaid order");
public IOrderState Cancel() => new Cancelled();
}
public sealed class Paid : IOrderState
{
public string Name => nameof(Paid);
public IOrderState MarkPaid() => throw new InvalidOperationException("Already paid");
public IOrderState Ship() => new Shipped();
public IOrderState Cancel() => new Cancelled();
}
public sealed class Shipped : IOrderState
{
public string Name => nameof(Shipped);
public IOrderState MarkPaid() => throw new InvalidOperationException("Already paid");
public IOrderState Ship() => throw new InvalidOperationException("Already shipped");
public IOrderState Cancel() => throw new InvalidOperationException("Cannot cancel shipped order");
}
public sealed class Cancelled : IOrderState
{
public string Name => nameof(Cancelled);
public IOrderState MarkPaid() => throw new InvalidOperationException("Cancelled");
public IOrderState Ship() => throw new InvalidOperationException("Cancelled");
public IOrderState Cancel() => this; // idempotent
}
public sealed class Order
{
private IOrderState _state = new Created();
public string Status => _state.Name;
public void MarkPaid() => _state = _state.MarkPaid();
public void Ship() => _state = _state.Ship();
public void Cancel() => _state = _state.Cancel();
}
Mỗi class state mã hoá rule cho state đó. Thêm operation mới
(Refund) nghĩa là thêm method trên interface và implement ở
mọi state — compiler nhắc bạn cả năm.
Bức tranh cấu trúc:
stateDiagram-v2
[*] --> Created
Created --> Paid : MarkPaid
Created --> Cancelled : Cancel
Paid --> Shipped : Ship
Paid --> Cancelled : Cancel
Shipped --> Delivered : Deliver
Cancelled --> [*]
Delivered --> [*]
Sơ đồ là tài liệu. Class mirror nó node-by-node.
Khi nào enum cộng pattern matching thay State?
Cho domain mà hành vi mỗi state nhỏ và bạn chủ yếu quan tâm
transition, enum cộng switch là thật thà:
public enum OrderStatus { Created, Paid, Shipped, Delivered, Cancelled }
public sealed class Order
{
public OrderStatus Status { get; private set; } = OrderStatus.Created;
public void MarkPaid() => Status = Status switch
{
OrderStatus.Created => OrderStatus.Paid,
_ => throw new InvalidOperationException($"Cannot pay from {Status}")
};
public void Ship() => Status = Status switch
{
OrderStatus.Paid => OrderStatus.Shipped,
_ => throw new InvalidOperationException($"Cannot ship from {Status}")
};
public void Cancel() => Status = Status switch
{
OrderStatus.Created or OrderStatus.Paid => OrderStatus.Cancelled,
OrderStatus.Cancelled => OrderStatus.Cancelled,
_ => throw new InvalidOperationException($"Cannot cancel from {Status}")
};
}
Đây là hình dạng đúng khi:
- Hành vi mỗi state chỉ là "transition nào cho phép" — không có hành vi phong phú ngoài status kế.
- Số state nhỏ (dưới 5–6) và khó mọc.
- Bạn muốn persist status thành một cột và round-trip sạch.
Hơn vậy, nâng lên class State — hoặc thư viện state-machine.
Thư viện Stateless làm State machine dễ chịu thế nào?
Stateless là NuGet nhỏ biến state-machine thành cấu hình fluent. Trigger, guard, callback on-entry, và generator graph Mermaid đều build sẵn:
public sealed class Order
{
private readonly StateMachine<OrderStatus, OrderTrigger> _sm;
public Order()
{
_sm = new StateMachine<OrderStatus, OrderTrigger>(OrderStatus.Created);
_sm.Configure(OrderStatus.Created)
.Permit(OrderTrigger.MarkPaid, OrderStatus.Paid)
.Permit(OrderTrigger.Cancel, OrderStatus.Cancelled);
_sm.Configure(OrderStatus.Paid)
.Permit(OrderTrigger.Ship, OrderStatus.Shipped)
.Permit(OrderTrigger.Cancel, OrderStatus.Cancelled)
.OnEntry(() => Console.WriteLine("Charge thành công; reserve inventory"));
_sm.Configure(OrderStatus.Shipped)
.Permit(OrderTrigger.Deliver, OrderStatus.Delivered);
}
public OrderStatus Status => _sm.State;
public void MarkPaid() => _sm.Fire(OrderTrigger.MarkPaid);
public void Ship() => _sm.Fire(OrderTrigger.Ship);
public void Deliver() => _sm.Fire(OrderTrigger.Deliver);
public void Cancel() => _sm.Fire(OrderTrigger.Cancel);
public string ToDotGraph() => Stateless.Graph.UmlDotGraph.Format(_sm.GetInfo());
}
public enum OrderTrigger { MarkPaid, Ship, Deliver, Cancel }
Ba thứ cách này cho miễn phí:
- Bảo đảm gần-compile-time.
Firecủa trigger không hỗ trợ throwInvalidOperationExceptionvới message rõ; cấu hình là source of truth duy nhất. - Hook OnEntry / OnExit. Side effect chạy tự động khi transition đáp; bạn không rải chúng khắp method body.
- Export sơ đồ.
ToDotGraph()round-trip vào Mermaid và tuyệt cho PR review.
Cho domain transition không tầm thường, library thắng.
Khi nào State pattern bắn trật?
Ba bẫy:
- Bùng nổ state. Mười hai tổ hợp "paid + shipped + cancelled-mid-flight + tax-pending" thành mười hai subclass. Tìm trục vuông góc; chúng thường gợi hai state machine, không phải một to.
- Class State với state mutable share giấu. Một instance
Paidgiữ counter share vớiShipped. Class là state nhưng counter không. Đẩy data share về originator (order); để hành vi trong class state. - Serialize object state. Object state thường chứa delegate hoặc reference không serialize được. Persist token state (enum hoặc string) cộng data liên quan; rehydrate object lúc load.
So sánh State với Strategy và Memento thế nào?
| Pattern | Đại diện cho | Khi swap |
|---|---|---|
| State | Giai đoạn lifecycle hiện tại của object | Transition nội bộ trên event |
| Strategy | Một thuật toán cắm được | Caller quyết định; thường lúc inject |
| Memento | Một snapshot state đông cứng để khôi phục | Biên save/restore |
Tách rõ: State là object đang là gì; Strategy là object tính toán thế nào; Memento là object đã là gì. Bạn thường kết hợp cả ba: state machine, mỗi state ủy thác strategy, cộng memento cho rollback transactional.
Một ví dụ thật trong .NET 10 trông thế nào?
Order Stateless-based đầy đủ với persistence. Token state là cột persist; state machine rebuild lúc load:
public enum OrderStatus { Created, Paid, Shipped, Delivered, Cancelled }
public enum OrderTrigger { MarkPaid, Ship, Deliver, Cancel }
public sealed class Order
{
private readonly StateMachine<OrderStatus, OrderTrigger> _sm;
private OrderStatus _status;
public Order(OrderStatus initial = OrderStatus.Created)
{
_status = initial;
_sm = new StateMachine<OrderStatus, OrderTrigger>(
stateAccessor: () => _status,
stateMutator: s => _status = s);
_sm.Configure(OrderStatus.Created)
.Permit(OrderTrigger.MarkPaid, OrderStatus.Paid)
.Permit(OrderTrigger.Cancel, OrderStatus.Cancelled);
_sm.Configure(OrderStatus.Paid)
.Permit(OrderTrigger.Ship, OrderStatus.Shipped)
.Permit(OrderTrigger.Cancel, OrderStatus.Cancelled);
_sm.Configure(OrderStatus.Shipped)
.Permit(OrderTrigger.Deliver, OrderStatus.Delivered);
}
public OrderStatus Status => _status;
public void MarkPaid() => _sm.Fire(OrderTrigger.MarkPaid);
public void Ship() => _sm.Fire(OrderTrigger.Ship);
public void Deliver() => _sm.Fire(OrderTrigger.Deliver);
public void Cancel() => _sm.Fire(OrderTrigger.Cancel);
}
// Persistence (EF Core)
public sealed class OrderRow
{
public int Id { get; set; }
public OrderStatus Status { get; set; } = OrderStatus.Created;
}
public sealed class OrderRepository
{
private readonly AppDbContext _db;
public OrderRepository(AppDbContext db) => _db = db;
public async Task<Order> LoadAsync(int id, CancellationToken ct)
{
var row = await _db.Orders.FindAsync(new object[] { id }, ct)
?? throw new NotFoundException();
return new Order(row.Status);
}
}
// Test
[Fact]
public void Cannot_ship_unpaid_order()
{
var sut = new Order();
Assert.Throws<InvalidOperationException>(() => sut.Ship());
}
[Fact]
public void Paid_order_can_be_shipped()
{
var sut = new Order(OrderStatus.Paid);
sut.Ship();
Assert.Equal(OrderStatus.Shipped, sut.Status);
}
Transition khai báo một lần. Persistence là một cột. Thêm
Refunded là hai giá trị enum, ba permit, zero method body.
Đọc tiếp gì trong series?
- Bài trước: Observer — thông báo publisher/subscriber trực tiếp.
- Bài kế: Strategy — cùng hình, ý đồ khác.
- Tham chiếu chéo: Command — đi cặp với State để ghi mọi transition cho audit và replay.
- Tham chiếu chéo: Memento — capture state trước transition rủi ro; khôi phục khi rollback.
- Cây quyết định: Cách chọn design pattern phù hợp.
Một takeaway thực dụng: lợi ích lớn nhất của State pattern là
làm transition không hợp lệ không thể diễn đạt được. Dù bạn
dùng hierarchy class, switch enum, hay Stateless, mục tiêu
giống: code cho phép MarkPaid() thành công chỉ từ state hợp
lệ, và từ chối ở mọi nơi khác. Làm đúng điều đó và cả category
bug biến mất.
Câu hỏi thường gặp
State khác Strategy ở điểm gì?
Shipped không thể bị trả tiền lại vì type class state hiện tại cấm.Khi nào enum đơn giản thắng hierarchy class State?
switch expression trên OrderStatus che hầu hết domain kiểu CRUD. Nâng lên class State khi mỗi state có hành vi có ý nghĩa, không chỉ data, hoặc khi transition cần là khái niệm hạng nhất.Nên dùng thư viện Stateless hay tự cuộn state machine?
OnEntry, OnExit, guard condition, sub-state, và sơ đồ text bạn paste vào PR description. Tự cuộn ổn cho case đơn giản nhất nhưng lao nhanh sang 'phát minh lại Stateless'.Persist state machine qua process thế nào?
StateAccessor / StateMutator đúng cho case này. Tránh serialize bản thân state machine — thường chứa delegate không sống được round trip.