Tổng quan Cơ bản 9 phút đọc

Design Patterns trong C# — Hướng dẫn cho người mới mà nhớ được

Tour cơ bản về design pattern trong C# / .NET 10: chúng là gì, khi nào dùng, và cách đọc 23 mẫu GoF kinh điển mà không bị ngợp thuật ngữ.

Mục lục
  1. Design pattern trong C# / .NET là gì?
  2. Vì sao design pattern vẫn quan trọng năm 2026?
  3. 23 pattern Gang of Four được chia nhóm thế nào?
  4. Đọc một mô tả pattern thế nào để khỏi bị ngợp?
  5. Khi nào nên tránh ép pattern vào code?
  6. Trông một ví dụ "trước - sau" siêu nhỏ ngay bây giờ thế nào?
  7. Đi tiếp trong series ở đâu?

Bạn đã thấy triệu chứng rồi. Một method ban đầu chỉ mười dòng dễ đọc, nay phình thành cái thang if/else 200 dòng đầy if (request.Type == "X"). Mỗi feature mới thêm một nhánh, mỗi bug-fix đụng vào ba nhánh, file test thì dài 2.000 dòng. Code chạy được. Không ai muốn mở nó ra.

Phần lớn lập trình viên đến đây sẽ Google "cách refactor switch dài". Họ thấy một kết quả nhắc đến Strategy pattern — và rời đi còn rối hơn lúc tới, vì lời giải thích là sơ đồ UML đầy ConcreteStrategyA, ConcreteStrategyB. Series này sinh ra để sửa cảnh đó. Chúng ta sẽ đi qua từng pattern Gang of Four bằng C# .NET 10 thật, triệu chứng thật, không có chỗ cho AbstractFactoryFactory lấp chỗ trống.

Bài đầu tiên này là tấm bản đồ. Đọc xong, bạn sẽ biết design pattern là gì, đọc một pattern thế nào cho đỡ ngợp, khi nào không nên dùng, và nên bắt đầu từ đâu trong những bài sau.

Design pattern trong C# / .NET là gì?

Một design pattern là giải pháp có tên, có thể tái sử dụng, cho một bài toán cứ tái diễn ở các codebase hướng đối tượng. Ba thứ làm nó trở thành "pattern" thay vì "lời khuyên":

  1. Có tên. "Strategy" là một từ mà hai senior nói trong PR review là hiểu nhau ngay. Cái tên chiếm một nửa giá trị.
  2. Có hình. Pattern mô tả các vai (interface, class, method) và cách chúng phối hợp. Vẽ được lên bảng.
  3. Có cái giá phải trả. Pattern liệt kê chi phí — thêm file, tăng tầng gián tiếp, có thể chậm hơn — để bạn cân nhắc trước khi áp dụng.

Trong .NET, bạn đã dùng cả chục pattern mà không gọi tên chúng. IEnumerable<T> là pattern Iterator. Middleware ASP.NET Core là Chain of Responsibility. IOptions<T> lai giữa StrategyAdapter. Lazy loading của EF Core là Proxy. Đội phát triển framework học trước những cái tên này để doc của họ gọn lại trong một trang; khi bạn học xong, doc của họ cũng tự ngắn đi đáng kể đối với bạn.

Vì sao design pattern vẫn quan trọng năm 2026?

Câu trả lời thật lòng không giống sách giáo trình đại học. Pattern quan trọng vì ba lý do thực dụng: giao tiếp, nhận diện, đòn bẩy refactor.

C# hiện đại có làm pattern thay đổi hình dạng. Record giúp Memento bất biến gần như miễn phí. Source generator thay được vài Visitor. Func<T, TResult> rút nhiều class Command/Strategy về một dòng. Ý đồ vẫn nguyên, phần boilerplate teo lại.

23 pattern Gang of Four được chia nhóm thế nào?

Năm 1994, bốn tác giả (Gamma, Helm, Johnson, Vlissides — tên gọi tắt là GoF) xuất bản cuốn sách đặt tên cho những pattern này và xếp chúng vào ba nhóm:

flowchart TB
    GOF["23 GoF Design Patterns"]
    GOF --> CRE["Khởi tạo · 5 pattern<br>object được tạo ra sao"]
    GOF --> STR["Cấu trúc · 7 pattern<br>object ghép với nhau ra sao"]
    GOF --> BEH["Hành vi · 11 pattern<br>object giao tiếp ra sao"]

    CRE --> C1[Singleton]
    CRE --> C2[Factory Method]
    CRE --> C3[Abstract Factory]
    CRE --> C4[Builder]
    CRE --> C5[Prototype]

    STR --> S1[Adapter]
    STR --> S2[Bridge]
    STR --> S3[Composite]
    STR --> S4[Decorator]
    STR --> S5[Facade]
    STR --> S6[Flyweight]
    STR --> S7[Proxy]

    BEH --> B1[Chain of Responsibility]
    BEH --> B2[Command]
    BEH --> B3[Interpreter]
    BEH --> B4[Iterator]
    BEH --> B5[Mediator]
    BEH --> B6[Memento]
    BEH --> B7[Observer]
    BEH --> B8[State]
    BEH --> B9[Strategy]
    BEH --> B10[Template Method]
    BEH --> B11[Visitor]

Cách nhóm này hữu ích hơn vẻ ngoài của nó:

Đặt được bài toán vào một trong ba nhóm trên là bạn đã thu hẹp tìm kiếm từ 23 pattern xuống còn khoảng 7. Riêng việc đó cũng đáng năm phút bỏ ra để nhớ sơ đồ phía trên.

Đọc một mô tả pattern thế nào để khỏi bị ngợp?

Mô tả pattern theo một khuôn cố định. Biết khuôn đó rồi, kể cả pattern lạ hoắc bạn cũng lướt qua được. Mọi bài trong series này đều dùng đúng các heading sau, một cách có chủ đích:

Mục Trả lời cho
Intent (Ý đồ) Một câu duy nhất "pattern này giải quyết bài toán nào".
Structure (Cấu trúc) Sơ đồ class — ai biết về ai.
Khi nào dùng Các triệu chứng cụ thể trong code.
Khi nào KHÔNG dùng Các bẫy — thường là "vừa mới học xong".
Ví dụ C# Một Program.cs chạy được, có output.
So sánh Phân biệt với pattern hay bị nhầm lẫn nhất.
Cạm bẫy Cái gì hỏng khi vào production.

Khi đọc một pattern mới, đọc theo thứ tự này: Intent → Khi nào dùng → Ví dụ C# → Cạm bẫy. Bỏ qua Cấu trúc trong lần đọc đầu. Sơ đồ chỉ có nghĩa sau khi bạn đã thấy code; đọc nó trước là lý do chính khiến người mới nghĩ pattern là chuyện trừu tượng vô bổ.

Khi nào nên tránh ép pattern vào code?

Đây là phần không ai để lên đầu bài, mà là phần bạn cần nhất.

Nguyên tắc của ba. Pattern xứng đáng với độ phức tạp của nó vào lần thứ ba bạn gặp lại bài toán cũ, không phải lần đầu. Lúc này chỉ có một thứ thay đổi? Một câu if là câu trả lời đúng. Hai? Vẫn nên là if. Ba? Lúc này mới hợp lý để tách Strategy hoặc Factory.

Cái búa của Maslow. Hai tuần sau khi học Visitor, bạn sẽ thấy bài toán Visitor ở khắp mọi nơi. Thực ra không phải. Hãy đợi triệu chứng — cái thang switch thật, đoạn clone trùng lặp thật — rồi mới đụng pattern.

Chi phí là có thật. Mỗi pattern thêm file, thêm tên gọi, thêm tầng gián tiếp. Người đọc code của bạn sáu tháng sau (thường là chính bạn) sẽ trả cái giá đó mỗi lần ghé. Nếu pattern không mang lại lợi ích cụ thể — khả năng thêm tính năng mà không sửa code cũ, có thể isolate để unit-test, loại trùng lặp thật — thì nó là thuế nhưng không có doanh thu.

Framework đã đóng gói sẵn nhiều pattern. Pipeline ASP.NET Core là Chain of Responsibility. DI container là Service Locator + Abstract Factory. MediatR là Mediator + Command. Nếu framework đã trao bạn pattern đó, không cần tự cuộn; cái cần là học API của framework.

Trông một ví dụ "trước - sau" siêu nhỏ ngay bây giờ thế nào?

Đây là triệu chứng mà chúng ta mở bài — một bộ tính chiết khấu cứ thêm nhánh if mỗi lần có hạng khách mới. Bản trước là cái team check-in vào một chiều thứ Ba dưới sức ép deadline:

// Trước — mỗi hạng mới là một nhánh + một bề mặt sinh bug.
public decimal CalculateDiscount(Customer customer, decimal subtotal)
{
    if (customer.Tier == "Standard")  return subtotal * 0.00m;
    if (customer.Tier == "Silver")    return subtotal * 0.05m;
    if (customer.Tier == "Gold")      return subtotal * 0.10m;
    if (customer.Tier == "Platinum")  return subtotal * 0.15m + 5m;
    if (customer.Tier == "Employee")  return subtotal * 0.30m;
    return 0m;
}

Pattern Strategy nói: "tách mỗi nhánh ra, đặt tên cho nó, để chỗ gọi tự chọn cái cần". Trong C# hiện đại, việc đó còn không cần class mới:

// Sau — mỗi luật một dòng, thêm hạng mới gói gọn một chỗ, dispatcher ngu ngơ.
using DiscountRule = System.Func<decimal, decimal>;

public static class DiscountRules
{
    public static readonly Dictionary<string, DiscountRule> ByTier = new()
    {
        ["Standard"] = sub => 0m,
        ["Silver"]   = sub => sub * 0.05m,
        ["Gold"]     = sub => sub * 0.10m,
        ["Platinum"] = sub => sub * 0.15m + 5m,
        ["Employee"] = sub => sub * 0.30m,
    };
}

public decimal CalculateDiscount(Customer customer, decimal subtotal)
    => DiscountRules.ByTier.TryGetValue(customer.Tier, out var rule)
        ? rule(subtotal)
        : 0m;

Bản "sau" chính là Strategy pattern, dù không hề có interface IDiscountStrategy nào. Cốt lõi của pattern là để phần thay đổi có thể thay thế được, không phải đẻ ra một hình dạng class cụ thể. Func<> của C# là đủ. Khi luật bắt đầu có trạng thái — cần log, cần gọi service khác, cần được unit-test riêng — đó là lúc bạn lên đời thành interface thật. Mà khi đó, bạn đã biết phải gọi tên nó thế nào.

Chúng ta sẽ quay lại đúng ví dụ này, với ràng buộc thực tế tăng dần, ở bài chuyên sâu về pattern Strategy.

Đi tiếp trong series ở đâu?

Series được sắp để đọc tuần tự từ trên xuống, nhưng bạn cũng có thể nhảy vào theo bài toán. Ba lộ trình gợi ý:

Một lưu ý cuối về cách đọc mỗi bài: mọi bài trong series có khối Quick Answer ngay dưới tiêu đề. Nếu bạn chỉ có 30 giây — giữa cuộc họp, đang review PR — đọc Quick Answer và FAQ là đủ. Quay lại đọc code khi rảnh. Cấu trúc đó là cố ý; design pattern phải làm bạn đọc nhanh hơn, không phải chậm hơn.

Câu hỏi thường gặp

C# hiện đại đã có record, pattern matching, minimal API thì design pattern còn cần không?
Vẫn cần — nhưng hình hài thay đổi. record khiến Memento và Prototype gần như miễn phí. Func<> và lambda thay luôn nhiều class Strategy hay Command. Ý đồ của pattern không đổi, chỉ có cách viết gọn hẳn lại. Biết pattern giúp bạn nhận ra khi nào một dòng Func<> là đủ và khi nào mới cần đến hệ thống interface đầy đủ.
Có cần học thuộc lòng cả 23 pattern Gang of Four không?
Không. Học chắc tám cái bạn gặp hằng tuần — Singleton, Factory Method, Builder, Adapter, Decorator, Observer, Strategy, Iterator — và rèn khả năng nhận diện phần còn lại. Khi gặp bài toán quen quen, bạn sẽ biết mở lại bài nào trong series. Kỹ năng nhận diện đó mới là đích thật sự.
Design pattern, nguyên lý SOLID và kiến trúc phần mềm khác nhau ở đâu?
Nguyên lý (SOLID, DRY) là quy tắc áp dụng ở mọi dòng code. Pattern là giải pháp có tên cho một loại bài toán ở mức vài class. Kiến trúc (microservices, hexagonal, MVC) là cách tổ chức cả hệ thống. Bạn có thể vi phạm nguyên lý mà không hỏng pattern và ngược lại.
Có nên refactor code cũ sang pattern chỉ vì mới nhận ra pattern đó không?
Hầu như không. Một pattern chỉ đáng độ phức tạp nó mang lại khi đoạn code có ít nhất hai lý do để cùng thay đổi theo cùng một hướng. Ép Strategy vào một câu if/else đơn lẻ chỉ làm khổ người đọc sau bạn. Hãy đợi đến trường hợp thứ hai mới đụng tay.