Bỏ qua và tới nội dung chính
Thiết kế contract và DSL

Cách viết contract đủ rõ cho thi công mà không biến thành tài liệu chết

Contract tốt không phải là tài liệu dài, mà là tài liệu khóa đúng phạm vi, dùng đúng ngôn ngữ và đủ cấu trúc để đội thi công có thể triển khai ít suy diễn. Bài viết này trình bày cách tách các lớp contract cốt lõi, viết ngắn phần nên ngắn, viết rõ phần bắt buộc phải rõ, và tránh biến contract thành giấy tờ vô dụng.

Huỳnh Kim Đạt Huỳnh Kim Đạt
4 lượt xem 8 phút đọc
Cách viết contract đủ rõ cho thi công mà không biến thành tài liệu chết

TL;DR

Muốn contract dùng được cho thi công, hãy tách các lớp domain, app, rule, workflow, policy và API; khóa rõ phạm vi và thuật ngữ; viết ngắn phần định hướng, viết có cấu trúc phần cần kiểm thử; tránh ôm quá nhiều và tránh bám sát code. Contract tốt giúp giảm suy diễn, tăng traceability và làm đầu ra ít bất ngờ hơn.

Key Takeaways

  • Contract tốt phải khóa rõ phạm vi quyết định và phạm vi không quyết định.
  • Nên tách sáu lớp contract cốt lõi: domain, app, rule, workflow, policy và API.
  • Rule, workflow, RBAC và schema là các phần bắt buộc phải viết có cấu trúc.
  • DSL contract tối giản giúp chuẩn hóa cách mô tả và tăng khả năng thi công.
  • Traceability bằng mã rule và workflow giúp contract không chết sau khi bắt đầu code.

Rất nhiều đội ngũ viết contract với hai trạng thái cực đoan: hoặc quá mơ hồ nên mỗi người hiểu một kiểu, hoặc quá dày và quá sát code khiến tài liệu nhanh chóng chết ngay sau vài vòng triển khai. Vấn đề không nằm ở việc có nên viết contract hay không, mà nằm ở cách thiết kế contract để nó đủ rõ cho thi công, đủ bền để còn dùng được sau khi code thay đổi, và đủ hẹp để không ôm mọi thứ.

Với cách tiếp cận contract-first trong một software factory như Midi Coder, mục tiêu của contract không phải là mô tả toàn bộ thế giới. Mục tiêu là giảm bất ngờ ở đầu ra: ai làm cũng hiểu cùng một phạm vi, cùng ràng buộc, cùng cách kiểm tra đúng sai, cùng dấu vết traceability từ yêu cầu đến code, test và vận hành.

1. Contract tốt là gì?

Một contract dùng được cho thi công thường có bốn đặc điểm:

  • Khóa đúng phạm vi: nói rõ nó quyết định phần nào và không quyết định phần nào.
  • Dùng ngôn ngữ ổn định: ưu tiên khái niệm nghiệp vụ, rule, workflow, policy và schema hơn là mô tả cảm tính.
  • Có cấu trúc để kiểm tra: mỗi phần có thể được map sang test, review hoặc một bước tự động hóa.
  • Dễ bảo trì: sửa ở một chỗ, tác động được nhìn thấy ở các chỗ liên quan.

Nếu một contract không giúp đội thi công trả lời các câu hỏi như “đầu vào là gì”, “điều kiện nào bắt buộc”, “quyền nào được phép”, “luồng nào hợp lệ”, “API trả gì khi lỗi”, thì contract đó chưa đủ rõ. Nếu nó cố giải thích luôn mọi quyết định UI, kiến trúc, coding style, lịch sử thay đổi và chi tiết class nội bộ, nó đang đi quá xa phạm vi cần thiết.

2. Sáu lớp contract cốt lõi nên tách riêng

Một lỗi phổ biến là nhét hết mọi thứ vào một tài liệu “đặc tả tổng”. Cách làm hiệu quả hơn là tách thành các lớp contract có trách nhiệm rõ ràng.

2.1. Domain contract

Định nghĩa ngôn ngữ nghiệp vụ: thực thể, thuộc tính, trạng thái, quan hệ, bất biến. Đây là lớp trả lời câu hỏi “đối tượng nào tồn tại trong bài toán và điều gì luôn đúng”.

  • Ví dụ tốt: định nghĩa một Request có các trạng thái draft, submitted, approved, rejected; không thể chuyển trực tiếp từ draft sang approved nếu chưa submitted.
  • Ví dụ kém: “yêu cầu được xử lý theo luồng phù hợp” nhưng không nói trạng thái nào tồn tại.

2.2. App contract

Mô tả các use case của ứng dụng: ai kích hoạt, đầu vào tối thiểu, đầu ra mong đợi, precondition, postcondition. Đây là lớp giúp đội thi công hiểu hệ thống phải làm gì ở mức hành vi.

2.3. Rule contract

Chứa business rule có thể kiểm tra được. Rule contract nên càng ít văn xuôi càng tốt, vì đây là nơi đội triển khai và QA cần độ rõ tuyệt đối.

  • Ví dụ: “Nếu loại hợp đồng là trial thì thời hạn tối đa là 30 ngày.”
  • Không nên viết: “Thời hạn trial nên ngắn để giảm rủi ro.” Câu này là ý định, không phải rule có thể kiểm tra.

2.4. Workflow contract

Mô tả các bước, trạng thái, tác nhân và điều kiện chuyển trạng thái. Đây là phần rất quan trọng nếu hệ thống có duyệt, giao việc, escalation, retry hoặc tích hợp nhiều vai trò.

2.5. Policy contract

Áp cho RBAC contract và các quyết định quyền hạn. Nó trả lời ai được xem, tạo, sửa, duyệt, xóa, export hay chạy hành động đặc biệt nào, trong điều kiện nào.

2.6. API contract

Mô tả giao tiếp kỹ thuật: endpoint, method, schema request/response, lỗi chuẩn, idempotency, pagination, versioning. Đây là phần cần chặt chẽ vì chỉ một mơ hồ nhỏ cũng có thể làm chậm cả frontend, backend và tích hợp.

Khi sáu lớp này được tách bạch, mỗi nhóm biết mình đang đọc phần gì. Domain không bị lẫn với API. Rule không bị chôn trong ghi chú. Workflow không bị mô tả bằng vài câu cảm tính. Policy không bị gắn rải rác trong code review.

3. Khóa phạm vi và khóa ngôn ngữ để contract dùng được cho thi công

Contract chết thường chết từ đầu, ngay ở phần mở bài, vì người viết không khóa phạm vi. Muốn tránh điều đó, nên chốt bốn thứ càng sớm càng tốt:

  1. Phạm vi quyết định: tài liệu này quyết định cái gì.
  2. Phạm vi không quyết định: cái gì nằm ngoài tài liệu này.
  3. Từ điển thuật ngữ: mỗi thuật ngữ chỉ có một nghĩa trong ngữ cảnh này.
  4. Nguồn sự thật: nếu có xung đột, phần nào được ưu tiên.

Một công thức mở đầu rất hữu ích là:

Scope:
- Quyết định: trạng thái, rule duyệt, quyền thao tác, schema API công khai.
- Không quyết định: kiến trúc module nội bộ, UI chi tiết, tên class, tên bảng.

Glossary:
- Request: hồ sơ nghiệp vụ do người dùng tạo.
- Approver: vai trò có quyền phê duyệt.
- Submit: hành động chuyển từ draft sang submitted.

Cách viết này giúp đội thi công biết họ nên bám vào đâu và không nên tranh luận lan sang đâu. Đây cũng là nền tảng để làm traceability: từ glossary sang rule, từ rule sang workflow, từ workflow sang API, từ API sang test.

4. 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 mọi phần trong contract đều cần mức chi tiết như nhau.

Nên viết ngắn

  • Bối cảnh: 1 đến 2 đoạn là đủ.
  • Mục tiêu: nêu rõ kết quả mong muốn, không kể lịch sử dài dòng.
  • Out of scope: liệt kê ngắn gọn, dứt khoát.
  • Nguyên tắc thiết kế: tối đa vài ý, tránh biến thành manifesto.

Bắt buộc phải rõ và có cấu trúc

  • Schema: field nào bắt buộc, kiểu dữ liệu, ràng buộc, enum.
  • Rule: điều kiện, ngoại lệ, độ ưu tiên giữa các rule.
  • Workflow: trạng thái, sự kiện, actor, guard condition.
  • Policy/RBAC: role, action, resource, condition.
  • API contract: request, response, status code, error code.

Nói ngắn gọn: phần nào cần kiểm thử, cần tích hợp, cần audit, cần phân quyền thì phải viết theo cấu trúc. Phần nào chỉ để định hướng thì nên gọn.

5. Một mẫu DSL contract tối giản nhưng đủ thi công

Nếu đội của bạn có nhiều module lặp lại, nên chuẩn hóa bằng DSL contract thay vì viết tự do mỗi nơi một kiểu. DSL không cần phức tạp; điều quan trọng là nhất quán.

domain Request {
  fields:
    id: uuid
    type: enum[trial, official]
    status: enum[draft, submitted, approved, rejected]
    owner_id: uuid
    duration_days: integer
}

rules {
  R1: if type = trial then duration_days <= 30
  R2: submit allowed only when owner_id exists
  R3: approve allowed only when status = submitted
}

workflow RequestApproval {
  draft -submit-> submitted
  submitted -approve-> approved
  submitted -reject-> rejected
}

policy {
  staff: create, read_own, update_own when status = draft
  approver: read_all, approve, reject when status = submitted
}

api POST /requests {
  request: RequestCreate
  response 201: RequestView
  response 422: ValidationError
}

Một DSL contract như vậy giúp nhiều việc diễn ra đồng thời: người phân tích đọc được, kỹ sư backend map sang validation và workflow engine được, frontend hiểu trạng thái để dựng màn hình, QA sinh test case được, và đội vận hành có căn cứ để trace một lỗi về đúng rule hoặc đúng policy.

6. Lỗi thường gặp khiến contract thành tài liệu chết

6.1. Cố ôm hết mọi thứ

Khi một tài liệu vừa muốn là BRD, vừa muốn là thiết kế kỹ thuật, vừa muốn là hướng dẫn UI, vừa muốn là checklist kiểm thử, nó sẽ nhanh chóng mất trọng tâm. Kết quả là không ai biết đâu là phần có tính quyết định.

6.2. Bám sát code quá mức

Nếu contract chứa tên class, tên hàm, tên biến nội bộ, nó sẽ hỏng theo từng lần refactor. Contract nên ổn định hơn code. Nó mô tả cam kết và hành vi, không phải ảnh chụp chi tiết cài đặt.

6.3. Viết rule bằng ngôn ngữ mơ hồ

Các cụm như “hợp lý”, “phù hợp”, “nếu cần”, “thông thường” gần như không có giá trị thi công. Rule phải được viết sao cho hai người khác nhau đọc vào vẫn kiểm tra ra cùng một kết luận.

6.4. Không có thứ tự ưu tiên khi rule xung đột

Ví dụ policy cho phép sửa bản ghi của mình, nhưng workflow lại cấm sửa sau khi đã submit. Nếu không nói rõ ưu tiên, đội thi công sẽ phải đoán. Và hễ có đoán là có bất nhất.

6.5. Không gắn traceability

Mỗi rule quan trọng nên có mã định danh như R1, R2, P1, WF1. Nhờ đó commit, test case, bug report, checklist review có thể trỏ về cùng một contract. Đây là điểm nhiều đội bỏ qua nhưng lại cực kỳ quan trọng trong mô hình software factory.

7. Contract tốt và contract tệ khác nhau ở đâu?

Ví dụ contract tệ

Người dùng tạo yêu cầu. Hệ thống kiểm tra hợp lệ và chuyển các bước phù hợp.
Người có quyền có thể duyệt. API trả lỗi nếu dữ liệu sai.

Đoạn này nghe có vẻ đúng, nhưng gần như không thi công được. Không biết “hợp lệ” là gì, “các bước phù hợp” là bước nào, “người có quyền” là ai, “lỗi” là lỗi nào.

Ví dụ contract tốt hơn

R1: Request.type = trial => Request.duration_days <= 30
R2: submit only when owner_id is not null
WF1: draft -submit-> submitted
WF2: submitted -approve-> approved
P1: staff can update own request only when status = draft
P2: approver can approve only when status = submitted
API-POST-/requests:
  201 => RequestView
  422/INVALID_DURATION when R1 violated
  403/FORBIDDEN when policy violated

Bản tốt hơn không dài hơn quá nhiều, nhưng mọi điểm quyết định đều đã hiện ra: rule, workflow, policy và API contract liên kết với nhau. Đó là khác biệt giữa tài liệu tham khảo chung chung và tài liệu có thể dùng để thi công.

8. Cách giữ contract sống sau khi bắt đầu code

Muốn contract không chết, đừng xem nó là tài liệu bàn giao một lần. Hãy biến nó thành một phần của vòng đời phát triển:

  • Review contract trước khi chia việc.
  • Gắn mã rule vào test case và pull request.
  • Đặt checklist rằng thay đổi workflow, policy hoặc API phải cập nhật contract tương ứng.
  • Ưu tiên contract có cấu trúc để có thể sinh schema, mock, validator hoặc tài liệu tích hợp.
  • Giữ một nguồn sự thật cho từng lớp contract thay vì copy đi nhiều nơi.

Trong thực tế, contract sống không nhất thiết phải hoàn hảo. Nó chỉ cần đủ ngắn để còn được cập nhật, đủ rõ để không ai phải đoán, và đủ cấu trúc để máy móc có thể hỗ trợ kiểm tra.

Kết luận

Contract tốt là contract làm cho code đầu ra ít bất ngờ hơn. Muốn vậy, hãy tách đúng lớp contract, khóa phạm vi ngay từ đầu, dùng ngôn ngữ kiểm tra được, viết có cấu trúc ở những phần ảnh hưởng trực tiếp đến thi công, và gắn traceability để mọi thay đổi đều lần ngược được về cam kết ban đầu.

Nếu phải nhớ một nguyên tắc duy nhất, hãy nhớ điều này: contract không cần dài để có giá trị; contract cần rõ, có ranh giới và có khả năng được sử dụng lặp lại trong quá trình làm sản phẩm.

References & Sources

  1. [1] Midicoder

Frequently Asked Questions

Làm sao biết contract đang quá dài?

Nếu tài liệu cố mô tả cả nghiệp vụ, UI, thiết kế kỹ thuật chi tiết, cấu trúc code và hướng dẫn vận hành trong cùng một chỗ, rất có thể nó đang quá dài và mất trọng tâm. Hãy tách theo lớp contract và giữ phần quyết định ở mức có thể kiểm tra được.

Contract có nên ghi tên class hoặc tên hàm không?

Thông thường không nên. Contract nên ổn định hơn code và mô tả cam kết, rule, workflow, policy, schema hoặc API. Tên class, tên hàm và chi tiết cài đặt nên nằm ở thiết kế kỹ thuật hoặc code.

Khi nào nên dùng DSL contract?

Nên dùng khi đội ngũ có nhiều module lặp lại, nhiều rule cần chuẩn hóa hoặc muốn tăng khả năng sinh validator, test, mock, schema và tài liệu tích hợp từ một nguồn sự thật chung.

RBAC contract nên viết ở đâu?

RBAC nên nằm trong policy contract, tách khỏi mô tả workflow và API. Mỗi role, action, resource và condition cần được mô tả rõ để tránh quyền bị hiểu khác nhau giữa các nhóm.