Enterprise Patterns, Real Code: Triển khai Ý tưởng của Fowler trong C# – Chris Woody Woodruff | Fractional Architect

Hầu hết các hệ thống doanh nghiệp hiện nay đều đã sử dụng các mẫu thiết kế (patterns) từ cuốn Patterns of Enterprise Application Architecture của Martin Fowler. Vấn đề là nhiều đội ngũ sử dụng chúng mà không gọi tên, triển khai một nửa rồi tự hỏi tại sao codebase lại chống đối.

Nếu giải pháp ASP.NET của bạn chứa những controller nói chuyện trực tiếp với SQL, những service trả về HTTP response, và những entity gọi SaveChanges, thì bạn đã đang remix các pattern của Fowler rồi đấy. Chỉ là bạn đang làm điều đó một cách ngầm định — và phải trả giá rất đắt.

Series bài viết này sẽ đi theo hướng ngược lại. Chúng ta sẽ gọi tên các pattern, chỉ ra chúng phù hợp ở đâu, và triển khai từng pattern trong C# với các ví dụ cụ thể.

Bạn sẽ thấy các pattern này giúp ích ở đâu, gây hại ở đâu, và cách chúng kết hợp thành những kiến trúc thực tế — thay vì những ảo tưởng trên sơ đồ.

Cách series này hoạt động

Trong mỗi bài viết, tôi sẽ:

  • Giải thích một pattern bằng ngôn ngữ đơn giản, với ý định gốc từ catalog của Fowler
  • Chỉ ra pattern đó thường xuất hiện thế nào trong các dự án C# và .NET
  • Đưa ra ví dụ tập trung về khi nào nên dùng và khi nào nên tránh
  • Kết nối nó với các pattern lân cận để bạn thấy được các lựa chọn thiết kế, chứ không phải những thủ thuật riêng lẻ

Bài giới thiệu này vẽ nên bản đồ toàn cảnh. Mỗi mục trong chỉ mục bên dưới đều liên kết đến một mô tả ngắn, một định nghĩa và một tình huống ví dụ. Mỗi phần đó sẽ trở thành một bài viết đầy đủ với code.

Chỉ mục các Pattern

Sử dụng các liên kết này để nhảy tới pattern bạn quan tâm:

Mỗi phần bên dưới vừa là bản xem trước vừa là một cam kết cho bài viết C# chuyên sâu sẽ theo sau.


Layered Architecture Pattern

Định nghĩa

Layered Architecture chia ứng dụng thành các tầng riêng biệt với trách nhiệm rõ ràng. Đường cơ sở của Fowler là:

  • Presentation: xử lý đầu vào và đầu ra
  • Domain: chứa các quy tắc nghiệp vụ và logic domain
  • Data source: quản lý lưu trữ và tích hợp với cơ sở dữ liệu

Quy tắc cốt lõi rất đơn giản và khắc nghiệt: nếu controller của bạn biết SQL hoặc repository của bạn biết HTTP, các tầng đã sụp đổ.

Khi nào nên sử dụng

Layered Architecture phù hợp với:

  • Các ứng dụng ASP.NET Core sẽ phát triển vượt quá một vài controller
  • Các hệ thống mà các đội ngũ khác nhau sở hữu UI, quy tắc nghiệp vụ và hạ tầng
  • Các codebase phải tồn tại qua nhiều lần thay đổi công nghệ trong vòng đời của chúng

Trong bài viết chuyên sâu, bạn sẽ thấy một giải pháp C# nơi các project align với các tầng, controller giữ mỏng, domain service không biết gì về transport, và repository đóng gói EF Core mà không làm rò rỉ nó lên trên.


Transaction Script Pattern

Định nghĩa

Transaction Script tổ chức logic nghiệp vụ dưới dạng các thủ tục xử lý một request hoặc use case duy nhất từ đầu đến cuối. Mỗi script:

  • Đọc đầu vào
  • Thực hiện tính toán và quyết định
  • Lưu trữ thay đổi

Có rất ít mô hình hóa domain. Trọng tâm vẫn là luồng của một giao dịch.

Khi nào nên sử dụng

Transaction Script hoạt động tốt nhất khi:

  • Logic domain đơn giản và nông
  • Bạn đang xây dựng báo cáo, tiện ích quản trị, hoặc công cụ di chuyển dữ liệu
  • Bạn cần kết quả nhanh chóng và độ phức tạp dài hạn có giới hạn

Trong C#, pattern này thường xuất hiện dưới dạng một application service hoặc handler class làm việc trực tiếp với DbContext và các DTO đơn giản. Trong bài viết, bạn sẽ thấy cả lợi ích và cái bẫy: nó có vẻ hiệu quả cho đến khi các quy tắc bắt đầu lặp lại giữa các script.


Domain Model Pattern

Định nghĩa

Domain Model tập trung các quy tắc nghiệp vụ bên trong một mô hình đối tượng giàu tính biểu đạt. Entity và value object thể hiện các bất biến (invariants) và hành vi.

Thay vì coi dữ liệu là các cấu trúc thụ động, bạn coi domain là trung tâm của lực hấp dẫn. Controller và repository quay xung quanh nó thay vì tiêm các quy tắc vào mọi rìa của hệ thống.

Khi nào nên sử dụng

Domain Model phát huy giá trị khi:

  • Các quy tắc nghiệp vụ phức tạp và phụ thuộc lẫn nhau
  • Các bất biến quan trọng hơn thông lượng thô
  • Bạn mong đợi các yêu cầu sẽ phát triển thường xuyên

Trong C#, điều này có nghĩa là các entity với phương thức thực thi quy tắc, factory kiểm soát việc tạo đối tượng, và service điều phối nhiều aggregate. Bài viết chuyên sâu sẽ cho thấy một aggregate trong code, cùng với các test khóa chặt hành vi trước khi bạn lo lắng về việc mapping EF.


Service Layer Pattern

Định nghĩa

Service Layer định nghĩa một tập hợp các thao tác ứng dụng nằm giữa thế giới bên ngoài và domain model. Nó:

  • Phối hợp nhiều domain object
  • Xử lý giao dịch và chính sách bảo mật
  • Cung cấp một API rõ ràng cho controller, message handler, hoặc các client khác

Đây là nơi các use case tồn tại.

Khi nào nên sử dụng

Service Layer phù hợp khi:

  • Bạn có nhiều client truy cập cùng một logic cốt lõi: web, background jobs, workers
  • Bạn muốn expose một application API ổn định trong khi UI phát triển
  • Các mối quan tâm xuyên suốt (cross-cutting concerns) như logging, phân quyền và ranh giới giao dịch phải nhất quán

Trong .NET, điều này thường trở thành một tập hợp các application service class được inject vào controller và worker. Trong bài viết, bạn sẽ thấy một service layer C# biến HTTP thành một chi tiết, chứ không phải ông chủ.


Active Record Pattern

Định nghĩa

Active Record hợp nhất domain object với persistence. Mỗi entity:

  • Ánh xạ trực tiếp đến một dòng trong cơ sở dữ liệu
  • Chứa logic nghiệp vụ
  • Biết cách tự load và lưu chính nó

Fowler coi nó là sự phù hợp chặt chẽ cho các domain đơn giản, nơi mô hình đối tượng phản chiếu gần với cơ sở dữ liệu.

Khi nào nên sử dụng

Active Record phù hợp với:

  • Các hệ thống nhỏ với các bảng đơn giản
  • Nguyên mẫu (prototype) nơi việc có thứ hoạt động quan trọng hơn sự trừu tượng sâu
  • Những nơi CRUD đơn giản với hành vi nhẹ là đủ

Trong C#, bạn thường thấy hương vị Active Record khi entity EF Core gọi SaveChanges trực tiếp hoặc các phương thức static thực hiện truy vấn toàn cục. Bài viết chuyên sâu sẽ cho thấy một phiên bản có kỷ luật của Active Record và giải thích khi nào nên loại bỏ nó để chuyển sang Data Mapper riêng biệt.


Data Mapper Pattern

Định nghĩa

Data Mapper nằm giữa domain object và cơ sở dữ liệu. Nó:

  • Load domain object từ data store
  • Lưu trữ các thay đổi trở lại
  • Bảo vệ domain khỏi kiến thức về cách persistence hoạt động

Các domain class vẫn không biết gì về persistence. Data Mapper gánh vác trách nhiệm dịch chuyển.

Khi nào nên sử dụng

Data Mapper mang lại giá trị khi:

  • Bạn có một Domain Model giàu tính biểu đạt
  • Lược đồ cơ sở dữ liệu phải phát triển độc lập với mô hình đối tượng
  • Bạn muốn test logic domain mà không cần cơ sở dữ liệu cản trở

Trong .NET, EF Core đã đóng vai trò Data Mapper. Bài viết sẽ chỉ ra cách thiết kế các domain class không phụ thuộc vào EF, sau đó map chúng bằng cấu hình và repository bọc mapper.


Repository Pattern

Định nghĩa

Repository cung cấp một giao diện giống như collection để truy cập các aggregate. Nó:

  • Ẩn các truy vấn và chi tiết persistence
  • Cung cấp các phương thức hoạt động bằng ngôn ngữ domain, như GetById, FindActiveForCustomer, Add, Remove
  • Cho phép domain nói bằng ngôn ngữ riêng của nó thay vì SQL hoặc query API

Fowler đưa Repository vào các object relational patterns như một cách để cô lập thêm logic domain khỏi truy cập dữ liệu.

Khi nào nên sử dụng

Repository hữu ích khi:

  • Cùng một aggregate xuất hiện trong nhiều use case
  • Bạn muốn các mẫu truy cập nhất quán cho các aggregate
  • Bạn kỳ vọng hỗ trợ nhiều chiến lược truy vấn hoặc kho lưu trữ khác nhau đằng sau cùng một domain interface

Trong C#, điều này thường có nghĩa là định nghĩa interface trong tầng domain và triển khai trong project infrastructure. Bài viết chuyên sâu sẽ bao gồm các thiết kế repository cụ thể, và cũng chỉ ra ví dụ về nơi repository tạo ra nhiều sự gián tiếp hơn lợi ích mà nó mang lại.


Unit of Work Pattern

Định nghĩa

Unit of Work theo dõi các thay đổi đối với domain object trong một giao dịch nghiệp vụ và ghi chúng ra dưới dạng một batch logic duy nhất. Nó:

  • Ghi lại các thao tác insert, update và delete
  • Phối hợp commit hoặc rollback
  • Cung cấp một ranh giới cho hành vi giao dịch

Fowler trình bày nó như một cách để ngăn chặn các thao tác ghi lan rộng không thể đoán trước trong codebase.

Khi nào nên sử dụng

Unit of Work có giá trị khi:

  • Một thao tác duy nhất chạm đến nhiều aggregate hoặc bảng
  • Bạn cần ranh giới giao dịch rõ ràng để đảm bảo tính nhất quán
  • Bạn muốn giữ logic domain không có các lời gọi save

Trong .NET, DbContext đã hoạt động như một Unit of Work, nhưng nhiều codebase lại che giấu điều đó. Bài viết sẽ chỉ ra cách embrace pattern này một cách tường minh và cách bọc EF Core trong một abstraction Unit of Work cấp cao hơn khi cần.


Identity Map Pattern

Định nghĩa

Identity Map đảm bảo rằng mỗi entity logic từ cơ sở dữ liệu chỉ tồn tại một lần trong bộ nhớ trên mỗi phạm vi. Nó:

  • Theo dõi các đối tượng đã load theo định danh
  • Trả về các instance hiện có thay vì tạo mới cho cùng một khóa
  • Giúp tránh các trạng thái trong bộ nhớ không nhất quán cho cùng một dòng

Pattern này thường hoạt động cùng với Unit of Work và Data Mapper.

Khi nào nên sử dụng

Identity Map quan trọng khi:

  • Cùng một entity được load qua các đường dẫn khác nhau trong một request
  • Bạn gắn hành vi domain vào entity và phụ thuộc vào tính bằng nhau của tham chiếu (reference equality)
  • Bạn quan tâm đến chi phí hiệu năng của việc materialization lặp lại

Các ORM như EF Core triển khai identity map dưới bề mặt. Bài viết chuyên sâu sẽ giải thích EF đang làm gì cho bạn và chỉ ra cách áp dụng Identity Map một cách tường minh khi bạn vận hành ngoài ORM hoặc sử dụng nhiều context.


Lazy Load Pattern

Định nghĩa

Lazy Load trì hoãn việc load dữ liệu liên quan cho đến khi nó thực sự cần. Thay vì tìm nạp toàn bộ đồ thị đối tượng, bạn:

  • Load một entity gốc
  • Biểu diễn các association như các placeholder
  • Kích hoạt việc load thực tế khi code truy cập association

Pattern này nhắm đến hiệu năng và bộ nhớ bằng cách tránh công việc không cần thiết.

Khi nào nên sử dụng

Lazy Load hữu ích khi:

  • Hầu hết các use case không cần đồ thị đầy đủ
  • Một số navigation tốn kém hoặc từ xa
  • Bạn phải kiểm soát sự bùng nổ truy vấn một cách cẩn thận

Trong .NET, EF Core có thể sử dụng lazy loading proxies, hoặc bạn có thể tự code các lazy association. Bài viết sẽ chỉ ra cả hai cách tiếp cận và làm nổi bật rủi ro: các truy vấn vô hình khiến bạn bất ngờ trong hồ sơ hiệu năng.


Front Controller Pattern

Định nghĩa

Front Controller tập trung hóa việc xử lý request cho một ứng dụng web. Thay vì để mỗi trang hoặc endpoint sở hữu entry point riêng, bạn:

  • Định tuyến tất cả request qua một handler duy nhất
  • Áp dụng logic xuyên suốt (cross-cutting) tại một chỗ
  • Ủy quyền cho controller hoặc handler để xử lý chi tiết

Fowler giới thiệu nó như một phản ứng đối với logic xử lý request bị trùng lặp.

Khi nào nên sử dụng

Front Controller phù hợp với:

  • Các ứng dụng web cần logging, xác thực và xử lý lỗi nhất quán
  • Các hệ thống phải đưa ra quyết định định tuyến dựa trên chính sách chung
  • Các kiến trúc sử dụng pipeline và middleware

Trong ASP.NET Core, sự kết hợp của hosting pipeline và routing đã tạo thành một Front Controller. Bài viết chuyên sâu sẽ chỉ ra cách kiểm soát pipeline đó một cách có chủ đích thay vì coi nó như phép thuật của framework.


Model View Controller (MVC) Pattern

Định nghĩa

Model View Controller chia logic UI thành ba phần:

  • Model: dữ liệu và hành vi nền tảng
  • View: logic hiển thị
  • Controller: xử lý đầu vào và điều phối

Phiên bản của Fowler tập trung vào server side web MVC.

Khi nào nên sử dụng

MVC phù hợp với:

  • Các ứng dụng có tương tác UI phức tạp phụ thuộc vào quy tắc domain
  • Các đội ngũ muốn có sự phân tách rõ ràng giữa logic trình bày và logic domain
  • Các hệ thống phải hỗ trợ nhiều view trên cùng một model

Trong ASP.NET Core MVC, controller nói chuyện với application service, view render model, và quy tắc domain không nằm ở cả hai nơi. Bài viết sẽ chỉ ra cách giữ code của controller gọn gàng thay vì để nó biến thành một tầng ứng dụng thứ hai.


Data Transfer Object (DTO) Pattern

Định nghĩa

Data Transfer Object mang dữ liệu qua các ranh giới tiến trình (process boundaries). Nó:

  • Tổng hợp các trường thành một hình dạng có thể serialize
  • Tránh gửi toàn bộ domain object qua wire
  • Cung cấp một hợp đồng giữa các service, client hoặc layer

DTO đánh đổi sự giàu có của đối tượng lấy sự ổn định và rõ ràng tại các điểm tích hợp.

Khi nào nên sử dụng

DTO đáng nỗ lực khi:

  • Bạn đang expose public API
  • Nhiều client tiêu thụ service của bạn, mỗi client có tốc độ phát triển riêng
  • Bạn muốn giữ các domain class nội bộ trong ứng dụng của mình

Trong C#, DTO thường xuất hiện dưới dạng record type trong API project hoặc message contract trong messaging system. Bài viết sẽ chỉ ra các mẫu mapping giữa domain object và DTO, cũng như cách giữ chúng không bị tràn ngập bởi độ phức tạp ngẫu nhiên (accidental complexity).


Điều gì tiếp theo

Phần còn lại của series này sẽ đi qua từng pattern:

  • Mỗi pattern có bài viết riêng
  • Mỗi bài viết bao gồm ví dụ C#, test khi có liên quan, và bối cảnh từ các dự án thực tế
  • Trọng tâm vẫn là sự đánh đổi (tradeoffs), chứ không phải tôn thờ sơ đồ

Bạn có thể đọc series từ đầu đến cuối, hoặc bạn có thể nhảy thẳng vào pattern phù hợp với nỗi đau trong hệ thống hiện tại của bạn và làm việc từ đó.

Nếu code của bạn đã giống các pattern này, series này sẽ cung cấp cho bạn ngôn ngữ và cấu trúc. Nếu không, các bài viết sắp tới sẽ chỉ ra cách tái cấu trúc nó từng phần một mà không cần tạm dừng việc bàn giao sản phẩm.

Tags: .NET, C#, dotnet, patterns, programming

Chỉ mục