Xem lại phần 4: DDD [Part 4]: Mô hình hóa tích hợp giữa các Bounded Context trong Domain-Driven Design
“Business logic là lý do phần mềm được xây ra.”
Giao diện có đẹp, cơ sở dữ liệu có nhanh đến đâu cũng trở nên vô nghĩa nếu phần mềm không giải quyết đúng bài toán kinh doanh.
Trong bài viết này, chúng ta sẽ cùng nhau tìm hiểu hai mẫu thiết kế đơn giản nhưng cực kỳ phổ biến trong việc tổ chức mã nguồn chứa business logic:
🎯 Transaction Script và 🧱 Active Record.
💡 Tại sao cần mẫu thiết kế cho business logic?
Business logic là phần quan trọng nhất trong một ứng dụng. Nó mô tả “luật chơi”, các quy tắc, hành động và cách xử lý dữ liệu đặc thù của lĩnh vực mà phần mềm phục vụ. Nhưng điều thú vị là: logic đơn giản lại dễ làm sai hơn ta tưởng, nhất là trong hệ thống có nhiều bước xử lý.
🔁 Transaction Script – Viết logic như thủ tục
📌 Định nghĩa:
Transaction Script là mẫu thiết kế trong đó mỗi thao tác nghiệp vụ được viết như một thủ tục độc lập.
Mỗi request từ giao diện người dùng (UI) tương ứng với một script xử lý nghiệp vụ tương ứng.
🔍 Cách hoạt động:
- Tổ chức thành các hàm/func xử lý yêu cầu.
- Có thể truy cập DB trực tiếp hoặc thông qua lớp truy cập dữ liệu đơn giản.
- Phải đảm bảo tính giao dịch (transactional): hoặc thành công toàn bộ, hoặc rollback toàn bộ.
Ví dụ:
DB.StartTransaction(); var job = DB.LoadNextJob(); var json = LoadFile(job.Source); var xml = ConvertJsonToXml(json); WriteFile(job.Destination, xml.ToString()); DB.MarkJobAsCompleted(job); DB.Commit();
✅ Ưu điểm:
- Dễ hiểu, dễ debug.
- Rất phù hợp với các subdomain phụ trợ, hoặc xử lý dạng ETL.
⚠️ Điểm chết người:
Thiếu transaction bao bọc → sai dữ liệu.
Ví dụ: cập nhật bảngUsers
, rồi ghi log vàoVisitsLog
. Nếu ghi log thất bại, DB sẽ mất đồng bộ.
Gửi message + lưu DB mà không có distributed transaction → rủi ro cao.
Retry tác vụ bị lỗi mà không idempotent → tăng gấp đôi dữ liệu.
💡 Gợi ý:
- Dùng
try/catch + rollback
. - Thiết kế các thao tác theo kiểu idempotent hoặc dùng optimistic concurrency control.
- Dùng mẫu Outbox pattern nếu cần đảm bảo giao tiếp giữa DB và message bus.
🧱 Active Record – Khi dữ liệu có cấu trúc phức tạp hơn
📌 Định nghĩa:
Một Active Record là một object:
- Đại diện cho một dòng dữ liệu trong bảng.
- Tự nó biết cách lưu, cập nhật, xóa, đọc chính nó.
- Có thể chứa logic nghiệp vụ đơn giản.
Ví dụ:
public void Execute(userDetails) { _db.StartTransaction(); var user = new User(); user.Name = userDetails.Name; user.Email = userDetails.Email; user.Save(); _db.Commit(); }
📦 Khi nào dùng:
- Khi logic đơn giản, nhưng dữ liệu phức tạp (nhiều quan hệ).
- Khi muốn kết hợp ORM để tránh viết lại thao tác truy xuất DB ở nhiều nơi.
✅ Ưu điểm:
- Gọn gàng, dễ kết hợp với framework ORM như ActiveRecord (Rails), Entity Framework (C#), Eloquent (Laravel)…
- Tăng khả năng tái sử dụng object.
⚠️ Lưu ý:
Nếu dùng sai – đặc biệt trong core domain, sẽ tạo ra mô hình “anemic domain model”: object chỉ là DTO không có logic thực sự.
Đừng cố ép Active Record vào logic phức tạp – nó không sinh ra để làm điều đó.
⚖️ Khi nào dùng Transaction Script vs Active Record?
Tình huống | Gợi ý mẫu thiết kế |
---|---|
Logic cực kỳ đơn giản, kiểu ETL hoặc xử lý 1 bước | ✅ Transaction Script |
CRUD đơn giản trên dữ liệu có quan hệ | ✅ Active Record |
Subdomain phụ, ít thay đổi, dễ hiểu | Cả hai dùng tốt |
Core domain, logic phức tạp, nhiều quy tắc nghiệp vụ | ❌ KHÔNG dùng hai mẫu này |
Cần xử lý logic lặp lại nhiều lần ở nhiều nơi | ❌ Tránh dùng Transaction Script nếu không muốn bị trùng logic |
🚨 Bài học từ lỗi thật
Xem đoạn code giả lập sau:
agent.ActiveTickets = agent.ActiveTickets + 1; agent.Save(); ticket.AssignedAgent = agent; ticket.Save(); _alerts.Send(agent, "You have a new ticket!");
Nếu một bước bị lỗi:
- Agent có thể bị tăng số lượng ticket mà không có ticket nào.
- Agent có thể nhận ticket nhưng không nhận được thông báo.
- Thao tác có thể bị retry và sinh ra dữ liệu sai gấp đôi.
📉 → Đây là các lỗi rất phổ biến khi triển khai transaction script mà không xử lý đúng transactional behavior.
✍️ Kết luận
Cả Transaction Script và Active Record đều là các công cụ hữu ích khi dùng đúng chỗ.
Nhưng nếu áp dụng cho core domain hoặc logic phức tạp, chúng sẽ biến thành con dao hai lưỡi dẫn đến kiến trúc khó bảo trì về sau.
💡 Lời khuyên:
Hãy chọn mẫu thiết kế phù hợp với độ phức tạp nghiệp vụ. Nếu đang bắt đầu với logic đơn giản, Transaction Script hoặc Active Record là lựa chọn tuyệt vời để khởi đầu—nhưng đừng gắn bó với nó mãi mãi.
📝 Kiểm tra kiến thức lại:
- Core domain nên dùng mẫu nào?
- Supporting subdomain nên dùng mẫu nào?
- Các lỗi tiềm ẩn trong CreateTicket
- Có edge case nào khác làm sai lệch hệ thống không?
Để lại một bình luận
Bạn phải đăng nhập để gửi bình luận.