1. Strategy — pattern bạn đã dùng suốt từ đầu tài liệu

Không cần dựng lại code evolution — bạn đã đi trọn con đường của nó ba lần: rule tính phí ship thành []FeeRule (1.1 → 1.4), gateway thanh toán thành interface + registry (2.2), rule hoa hồng thành []RateRule (3.4). Chỉ cần chính danh hóa:

Intent (GoF): đóng gói một họ thuật toán thay thế được cho nhau, để thuật toán biến thiên độc lập với client dùng nó. Cấu trúc GoF: interface Strategy + N concrete class + context giữ tham chiếu strategy.

Hình dạng Go hiện đại — thang ba bậc, chọn thấp nhất đủ dùng (đúc kết từ 1.4):

// Bậc 1 — function value: strategy KHÔNG STATE, một thao tác
type FeeRule func(Order) int64
// → 90% nhu cầu strategy trong code nghiệp vụ dừng ở đây

// Bậc 2 — interface 1-2 method: strategy CÓ STATE/CONFIG hoặc cần định danh
type Compressor interface {
    Compress(dst io.Writer, src io.Reader) error
    Ext() string                    // strategy tự mô tả — function value không làm được
}

// Bậc 3 — interface + registry: danh sách strategy MỞ cho bên ngoài (2.2, 4.1)

Câu hỏi phân bậc: strategy có cần state/config riêng không? có cần nhiều method không? ai cung cấp strategy mới? — leo bậc khi câu trả lời đòi hỏi, không leo trước.

TS đối chiếu: y hệt — callback/closure là bậc 1 mặc định (array.sort(comparator) chính là Strategy); class chỉ khi có state đáng kể.

2. State — cấu trúc của Strategy, bài toán của vòng đời

Code evolution — nơi State thật sự sinh ra

Bài toán chạy xuyên tài liệu, giờ nhìn vào khía cạnh chưa mổ: vòng đời đơn hàng. pending → confirmed → shipped → delivered, cộng cancelledreturned. V1 mà mọi codebase đều có:

// ❌ V1 — mỗi hành động một hàm, mỗi hàm một rừng check trạng thái
func (o *Order) Cancel() error {
    if o.Status == "shipped" || o.Status == "delivered" {
        return errors.New("cannot cancel shipped order")
    }
    if o.Status == "cancelled" {
        return errors.New("already cancelled")
    }
    if o.Status == "confirmed" && o.PaymentCaptured {
        o.refund()                        // hủy sau khi thu tiền → phải hoàn
    }
    o.Status = "cancelled"
    return nil
}

func (o *Order) Ship() error   { /* rừng if khác, trùng một phần */ }
func (o *Order) Return() error { /* rừng if khác nữa */ }

Quỹ đạo xuống cấp có mùi riêng: mỗi trạng thái mới (thêm on_hold) phải rà mọi hàm hành động xem có nhánh nào bị ảnh hưởng — tri thức về một trạng thái bị rải theo chiều ngang khắp các hàm (Shotgun Surgery theo trục trạng thái); bảng chuyển trạng thái hợp lệ — tài sản nghiệp vụ quan trọng nhất của module — không tồn tại ở đâu cả, nó là tổng gộp ngầm của các câu if.

Hai lời giải — và lời khuyên trung thực về thứ tự thử

Lời giải 1 — bảng chuyển trạng thái tường minh. Chưa cần pattern:

// ✅ V2 — bảng chuyển hợp lệ: tài sản nghiệp vụ thành DATA đọc được, review được
var transitions = map[Status][]Status{
    Pending:   {Confirmed, Cancelled},
    Confirmed: {Shipped, Cancelled, OnHold},
    Shipped:   {Delivered, Returned},
    /* ... */
}

func (o *Order) transitionTo(next Status) error {
    if !slices.Contains(transitions[o.status], next) {
        return fmt.Errorf("invalid transition %s → %s", o.status, next)
    }
    o.status = next
    return nil
}

func (o *Order) Cancel() error {
    if err := o.transitionTo(Cancelled); err != nil {
        return err
    }
    if o.paymentCaptured { o.refund() }   // side effect vẫn if — còn ít thì còn ổn
    return nil
}

Với đa số module backend, V2 là điểm dừng đúng: bảng chuyển thành single source of truth, thêm trạng thái = thêm dòng vào bảng, PM đọc được bảng để đối chiếu nghiệp vụ. V2 chỉ hụt hơi khi hành vi trong mỗi trạng thái phân hóa mạnh — không chỉ “được chuyển đi đâu” mà cả “Cancel lúc này làm gì” khác hẳn nhau theo trạng thái.

Lời giải 2 — State pattern. Khi mỗi trạng thái là một cụm hành vi thật sự:

// ✅ V3 — mỗi trạng thái MỘT TYPE, gom trọn hành vi của nó về một nhà
type orderState interface {
    Cancel(o *Order) error
    Ship(o *Order) error
    Return(o *Order) error
}

type confirmedState struct{}
func (confirmedState) Cancel(o *Order) error {
    if o.paymentCaptured { o.refund() }
    o.setState(cancelledState{})          // ← trạng thái TỰ quyết định chuyển đi đâu
    return nil
}
func (confirmedState) Ship(o *Order) error   { o.setState(shippedState{}); /*...*/ return nil }
func (confirmedState) Return(o *Order) error { return ErrInvalidAction }

type shippedState struct{}
func (shippedState) Cancel(o *Order) error { return errors.New("cannot cancel shipped order") }
/* ... */

// Order ủy quyền mọi hành động cho state hiện tại — không còn MỘT câu if trạng thái nào
func (o *Order) Cancel() error { return o.state.Cancel(o) }

Đọc confirmedState là thấy toàn bộ những gì có thể xảy ra ở trạng thái confirmed — tri thức gom theo chiều dọc, thêm trạng thái mới = thêm một type (OCP theo trục trạng thái). Giá phải trả: N trạng thái × M hành động method (đa số trả ErrInvalidAction — boilerplate); bảng chuyển tổng thể lại tan vào các method (V2 có bảng, V3 mất bảng — muốn cả hai phải kỷ luật tài liệu); và serialize state object ↔ cột status trong DB cần mapping thêm.

Ma trận quyết định:

Hành vi theo trạng thái đơn giản, bảng chuyển là tài sản chính  → V2 (bảng + if)
Hành vi phân hóa mạnh, mỗi trạng thái một thế giới con          → V3 (State pattern)
Vòng đời phức tạp + phân tán + cần audit từng bước chuyển       → event sourcing / workflow
                                                                   engine (Level 5, Saga)

3. So sánh khách quan: Strategy vs State

Cấu trúc UML giống hệt nhau (context ủy quyền cho interface, N implementation) — phân biệt nằm trọn trong ba câu hỏi runtime:

StrategyState
Ai chọn implementation?Client/caller — chọn lúc lắp ráp, từ bên ngoàiChính các state — mỗi state quyết định chuyển tiếp đi đâu, từ bên trong
Đổi implementation giữa chừng?Hiếm — chọn xong thường giữ nguyên vòng đờiLiên tục — chuyển trạng thái là bản chất của pattern
Các implementation có biết nhau?Không — các strategy độc lập, không quan tâm nhau — state này nắm luật chuyển sang state kia
Câu nghiệp vụ tương ứng“Tính phí bằng cách nào?”“Đơn hàng đang ở đâu trong vòng đời?”
Productionsort comparator, retry policy, compression codecTCP connection state, order lifecycle, circuit breaker (closed→open→half-open)

Circuit breaker là ví dụ chốt đáng nhớ: ba trạng thái, hành vi Call() khác hẳn nhau (closed: gọi thật + đếm lỗi; open: fail ngay; half-open: cho một request thăm dò), và tự chuyển theo kết quả — State pattern giáo khoa sống trong mọi thư viện resilience (gobreaker, resilience4j, opossum).

4. So sánh khách quan: Interface vs Generics — chọn cơ chế đa hình nào

Chương 1.4 đã cho quy tắc nhanh; giờ đủ ngữ cảnh để mổ đầy đủ — vì Strategy chính là nơi câu hỏi này nổi lên nhiều nhất từ Go 1.18:

// Cùng một bài "xử lý theo kiểu khác nhau" — hai cơ chế:

// Interface — dispatch RUNTIME: các kiểu KHÁC NHAU sống chung một danh sách
func Render(shapes []Shape) { for _, s := range shapes { s.Draw() } }
// []Shape chứa Circle lẫn Square — quyết định Draw nào chạy: lúc chạy, từng phần tử

// Generics — đơn hình hóa COMPILE-TIME: mỗi lần dùng chốt MỘT kiểu
func Sum[T Number](xs []T) T { var s T; for _, x := range xs { s += x } return s }
// Sum[int] và Sum[float64] là hai hàm riêng sau compile — []T thuần nhất một kiểu
Tiêu chíInterfaceGenerics
Quyết định kiểu lúcRuntime (dynamic dispatch)Compile-time (monomorphization)
Hỗn hợp kiểu trong một collection✅ bản chất❌ một instance một kiểu
Hành vi khác nhau theo kiểu✅ mỗi type tự implement❌ generics đòi hành vi giống nhau, chỉ kiểu dữ liệu khác
Hiệu năngGián tiếp qua itable; giá trị có thể escape lên heapCode chuyên biệt hóa, inline được — nhanh hơn ở hot path
Chi phí trừu tượngThấp, quen thuộcChữ ký phức tạp nhanh; constraint lan truyền qua call chain
Bài toán mẫuStrategy, plugin, DI, mock — hành vi biến thiênContainer, slice/map helper, thuật toán số — kiểu biến thiên, hành vi bất biến

Câu thần chú của cộng đồng Go: “generics cho kiểu, interface cho hành vi” — viết Strategy bằng generics (Processor[T PaymentMethod]) là dùng sai công cụ: bạn mất khả năng chứa hỗn hợp và mất dispatch runtime, đổi lấy tối ưu không cần thiết. Ngược lại, viết Map/Filter bằng interface + type assertion (kiểu Go cũ) là trả giá runtime cho thứ compile-time làm được sạch hơn. Hai công cụ trực giao, không cạnh tranh — và kết hợp được: func SortBy[T any](xs []T, less func(a, b T) bool) — generics giữ kiểu, function value (strategy) giữ hành vi: chính là chữ ký của slices.SortFunc.

TS đối chiếu: ranh giới mờ hơn (generics TS chỉ là type-level, không monomorphize, không khác biệt hiệu năng) — nên bên TS câu hỏi này nhẹ; bên Go nó là quyết định thiết kế thật với hệ quả đo được.

5. Anti-pattern & khi nào KHÔNG dùng

  • Strategy cho thứ không biến thiên: interface TaxCalculator + đúng một VNTaxCalculator ba năm — bậc thang leo trước bằng chứng (đã điểm mặt ở 1.5, 4.1; nhắc vì Strategy là nơi phạm nhiều nhất).
  • State pattern cho hai trạng thái: active/inactive với một câu if — V3 cho bài toán V1 chưa có.
  • Strategy chọn bằng switch bên trong context: context nhận tên rồi switch name { case "a": s = &A{} } ngay trong method nghiệp vụ — chọn strategy là việc của composition root/factory; trộn vào context là mất luôn lợi ích tách biệt.
  • Máy trạng thái ngầm bằng boolean: isPaid, isShipped, isCancelled, isReturned — 4 boolean = 16 tổ hợp trong khi chỉ 6 hợp lệ; các tổ hợp vô nghĩa (isCancelled && isShipped) tồn tại được trong bộ nhớ và DB. Một enum trạng thái + bảng chuyển (V2) là thuốc — “make invalid states unrepresentable” (1.3/1.5) áp cho vòng đời.

Tiếp theo: 4.8 — Observer & pub/sub