Trong nhiều đội phát triển, contract thường rơi vào một trong hai thái cực: hoặc quá dài đến mức không ai đọc hết, hoặc quá mỏng đến mức mỗi người hiểu một kiểu. Vấn đề không nằm ở độ dài. Vấn đề nằm ở việc contract có khóa được những quyết định quan trọng hay không.
Một contract tốt là contract làm cho đầu ra của quá trình thi công ít bất ngờ hơn. Nó không cần ôm toàn bộ thế giới. Nó cần xác định rõ phạm vi, ngôn ngữ, ràng buộc và điểm giao tiếp giữa những người cùng tham gia xây dựng sản phẩm. Đó cũng là tinh thần của contract-first: quyết định cái gì phải ổn định trước, cái gì có thể thay đổi sau, và cái gì cần truy vết xuyên suốt từ business đến code.
Trong cách làm của Midi Coder và các mô hình software factory, contract không chỉ là tài liệu mô tả. Contract là vật mang quyết định, là đầu vào cho thi công, là cơ sở để traceability, và trong nhiều trường hợp còn là nền cho DSL contract, workflow contract, RBAC contract hay API contract.
Contract tốt là contract khóa đúng quyết định
Nếu mọi thứ đều được đưa vào contract, contract sẽ biến thành bãi chứa thông tin. Nếu không khóa gì cả, contract chỉ còn là lời kể. Do đó câu hỏi quan trọng nhất khi thiết kế contract không phải là viết bao nhiêu, mà là cần khóa quyết định nào để giảm mơ hồ.
Thông thường, một contract tốt nên khóa các nhóm quyết định sau:
- Phạm vi: bài toán này bao gồm gì, không bao gồm gì.
- Ngôn ngữ: thuật ngữ nào là chuẩn, khái niệm nào bị cấm dùng lẫn nhau.
- Ràng buộc nghiệp vụ: điều kiện đúng sai, ngoại lệ, giới hạn, trạng thái.
- Luồng xử lý: bước nào bắt buộc, ai được kích hoạt, khi nào chuyển trạng thái.
- Phân quyền: ai được xem, tạo, sửa, duyệt, hủy.
- Điểm giao tiếp kỹ thuật: input, output, schema, version, lỗi, tính tương thích.
Nếu một contract khóa được các quyết định này, đội phát triển sẽ ít tranh cãi lại những điều đã chốt, QA có chỗ bám để kiểm thử, và hệ thống dễ được triển khai theo hướng nhất quán hơn.
Các lớp contract cốt lõi
Một sai lầm phổ biến là dùng một loại contract để giải quyết mọi vấn đề. Trên thực tế, contract nên được tách thành các lớp, mỗi lớp giữ một vai trò rõ ràng.
1. Domain contract
Domain contract định nghĩa ngôn ngữ của bài toán: thực thể, thuộc tính, trạng thái, quan hệ, thuật ngữ. Đây là nơi khóa nghĩa của từ. Ví dụ, nếu hệ thống có các khái niệm như đơn nháp, đơn chờ duyệt, đơn đã phát hành, thì domain contract phải làm rõ mỗi trạng thái có nghĩa gì và khác nhau ở đâu.
Một domain contract tốt không cần kể chuyện dài dòng. Nó cần chính xác về từ vựng. Khi từ vựng sai, mọi lớp phía sau đều sai theo.
2. App contract
App contract mô tả các năng lực mà ứng dụng cung cấp cho người dùng hoặc hệ thống khác. Đây là lớp trả lời câu hỏi: người dùng có thể làm gì với hệ thống, theo các trường hợp sử dụng nào, với đầu vào và kết quả mong đợi ra sao.
App contract không nên sa vào chi tiết implementation. Nó nên đủ cụ thể để đội thi công biết mình đang xây cái gì.
3. Rule contract
Rule contract giữ các quy tắc nghiệp vụ có thể kiểm chứng. Đây là phần đặc biệt quan trọng vì nó trực tiếp ảnh hưởng đến hành vi của hệ thống. Ví dụ: một yêu cầu chỉ được duyệt khi đã có đủ hồ sơ, hoặc giá trị vượt ngưỡng phải qua hai lớp phê duyệt.
Quy tắc nên được viết theo cấu trúc rõ ràng, có điều kiện, có kết quả, có ngoại lệ nếu cần. Rule contract là nơi rất phù hợp để biểu diễn bằng DSL contract nếu muốn tăng tính nhất quán và khả năng tự động hóa.
4. Workflow contract
Workflow contract khóa luồng chuyển trạng thái và trách nhiệm xử lý. Nó mô tả bước nào diễn ra trước, bước nào diễn ra sau, điều kiện nào cho phép chuyển bước, và tín hiệu nào đánh dấu hoàn tất.
Khi không có workflow contract rõ ràng, hệ thống thường phát sinh xử lý vòng vo, nhảy bước hoặc chồng chéo trách nhiệm giữa các vai trò.
5. Policy contract
Policy contract giữ các chính sách vận hành như RBAC contract, hạn mức, điều kiện kiểm soát, quy định tuân thủ. Lớp này tách phần chính sách ra khỏi chi tiết use case giúp hệ thống dễ mở rộng và kiểm soát hơn.
RBAC contract đặc biệt quan trọng vì nó giúp khóa quyền theo vai trò thay vì để logic quyền rải rác trong code. Khi policy rõ, việc kiểm thử quyền và truy vết thay đổi cũng rõ hơn.
6. API contract
API contract là điểm giao tiếp kỹ thuật giữa các thành phần. Nó cần rõ schema, kiểu dữ liệu, ràng buộc, mã lỗi, tương thích ngược, version và ví dụ trao đổi dữ liệu. Đây là lớp contract gần code nhất, nhưng không nên bị đồng nhất với code.
API contract tốt giúp frontend, backend, QA và tích hợp làm việc song song. Tuy vậy, một API contract tốt chỉ thật sự phát huy khi phía trên nó đã có domain, rule và workflow đủ rõ.
Cách khóa phạm vi và ngôn ngữ để contract dùng được cho thi công
Nhiều contract thất bại không phải vì thiếu thông tin mà vì không khóa phạm vi và ngôn ngữ. Khi đó người đọc tưởng như đã hiểu nhưng thực ra mỗi người đang dùng một mô hình khác nhau trong đầu.
Để contract dùng được cho thi công, cần làm ít nhất bốn việc:
- Khóa biên giới bài toán: nêu rõ trong phạm vi và ngoài phạm vi.
- Khóa từ vựng: một thuật ngữ chỉ có một nghĩa trong ngữ cảnh của contract.
- Khóa cấu trúc: phần nào là định nghĩa, phần nào là quy tắc, phần nào là ví dụ.
- Khóa khả năng thay đổi: mục nào ổn định, mục nào có thể điều chỉnh theo version.
Đây là lý do các đội theo contract coding thường ưu tiên cấu trúc hóa contract thành schema, bảng quyết định, ma trận quyền, state machine, hoặc DSL thay vì chỉ viết văn xuôi. Văn xuôi hữu ích cho bối cảnh. Nhưng quyết định quan trọng nên được đặt trong cấu trúc để giảm diễn giải tự do.
Phần nào nên viết ngắn, phần nào bắt buộc phải rõ và có cấu trúc
Không phải phần nào cũng cần dài. Thực tế, nhiều phần càng ngắn càng tốt.
Nên viết ngắn
- Mục tiêu của bài toán.
- Phạm vi áp dụng.
- Nguyên tắc thiết kế chính.
- Giả định và ràng buộc cấp cao.
Các phần này nên ngắn vì mục đích là định hướng, không phải tranh luận học thuật.
Bắt buộc phải rõ và có cấu trúc
- Định nghĩa thực thể và trường dữ liệu quan trọng.
- Quy tắc nghiệp vụ có điều kiện.
- Luồng trạng thái và điều kiện chuyển tiếp.
- Ma trận quyền và trách nhiệm.
- Schema trao đổi dữ liệu và lỗi.
Nếu các phần này vẫn viết mơ hồ, contract sẽ không đủ sức khóa quyết định. Đội thi công buộc phải tự lấp khoảng trống bằng suy đoán. Kết quả là sản phẩm ra đời nhanh nhưng thiếu nhất quán và khó truy vết.
Lỗi thường gặp khi contract ôm hết mọi thứ hoặc bám sát code quá mức
Ôm hết mọi thứ
Khi contract cố bao phủ toàn bộ chi tiết sản phẩm, nó nhanh chóng trở thành tài liệu khó bảo trì. Người viết không còn phân biệt đâu là quyết định cốt lõi, đâu là thông tin tham khảo. Mỗi lần thay đổi nhỏ đều kéo theo cập nhật dây chuyền, khiến tài liệu nhanh lỗi thời.
Biểu hiện dễ thấy là contract có rất nhiều mô tả nhưng rất ít phần có thể kiểm chứng trực tiếp.
Bám sát code quá mức
Ở chiều ngược lại, có những contract thực chất chỉ là bản sao của implementation hiện tại: tên class, tên hàm, cấu trúc bảng, chi tiết framework. Cách viết này làm contract mất vai trò kiến trúc. Khi code thay đổi, contract đổi theo, nhưng những quyết định ở tầng domain hay policy vẫn không được phát biểu rõ.
Một contract tốt nên gần enough để thi công, nhưng xa enough để không bị trói vào chi tiết triển khai tạm thời.
Ví dụ: contract tốt và contract tệ khác nhau ở đâu
Ví dụ contract tệ
Yêu cầu duyệt đơn: Hệ thống cho phép người dùng gửi đơn và quản lý có thể duyệt. Nếu hợp lệ thì duyệt, nếu không thì từ chối. Hệ thống cần lưu lịch sử và phân quyền phù hợp.
Đoạn mô tả này nghe có vẻ ổn nhưng gần như không khóa được quyết định nào. “Hợp lệ” là gì? Ai là “quản lý”? Có bao nhiêu trạng thái? Có được duyệt lại không? Lưu lịch sử đến mức nào? Phân quyền theo người hay theo vai trò?
Ví dụ contract tốt
Domain: Đơn có các trạng thái draft, submitted, approved, rejected, cancelled.
Rule: Đơn chỉ được chuyển từ draft sang submitted khi đủ 3 trường bắt buộc: người yêu cầu, lý do, chi phí ước tính. Đơn có chi phí lớn hơn 50.000.000 phải qua hai cấp duyệt.
Workflow: Người tạo đơn gửi đơn. Trưởng bộ phận duyệt cấp 1. Nếu vượt ngưỡng, giám đốc duyệt cấp 2. Bất kỳ cấp nào từ chối thì trạng thái chuyển sang rejected và bắt buộc có lý do.
RBAC contract: Vai trò requester được tạo, sửa đơn khi ở draft; vai trò manager được duyệt cấp 1; vai trò director được duyệt cấp 2; vai trò auditor chỉ được xem lịch sử.
API contract: API nộp đơn nhận payload theo schema xác định, trả về mã lỗi VALIDATION_ERROR nếu thiếu trường bắt buộc, FORBIDDEN nếu vai trò không hợp lệ.
Ví dụ thứ hai ngắn hơn nhiều tài liệu dày hàng chục trang, nhưng nó khóa được các quyết định cốt lõi. Đó là khác biệt giữa contract dùng để thi công và tài liệu chỉ để tham khảo.
Từ tài liệu sang năng lực thi công
Khi contract được tổ chức tốt, đội ngũ có thể dùng nó như nền cho contract-first delivery: phân tích, thiết kế, sinh test case, tạo checklist review, kiểm tra traceability, và thậm chí sinh một phần code hoặc cấu hình nếu DSL đủ chặt.
Đó cũng là điểm mạnh của mô hình software factory: tri thức không chỉ nằm trong đầu từng cá nhân mà được đóng gói thành contract có cấu trúc, có khả năng tái sử dụng, kiểm tra và mở rộng.
Với Midi Coder, cách tiếp cận này đặc biệt phù hợp cho các hệ thống cần phối hợp nhiều lớp quyết định: domain contract để khóa ngôn ngữ, workflow contract để khóa vận hành, RBAC contract để khóa quyền, API contract để khóa giao tiếp kỹ thuật. Khi các lớp này liên kết được với nhau, traceability trở nên thực tế hơn thay vì chỉ là khẩu hiệu.
Kết luận
Contract tốt không cần dài. Contract tốt cần khóa được những quyết định mà nếu không khóa, đội thi công sẽ hiểu khác nhau và làm ra kết quả khó đoán. Viết ít nhưng đúng chỗ sẽ giá trị hơn viết nhiều nhưng không kiểm chứng được.
Nếu phải nhớ một tiêu chí duy nhất, hãy nhớ điều này: contract tốt là contract khiến code đầu ra ít bất ngờ hơn. Khi contract làm được điều đó, nó không còn là giấy tờ. Nó trở thành một phần thực sự của năng lực xây dựng sản phẩm.