AllianceProject Handbook
← Knowledge

Kiểm soát chất lượng

Ranh giới kiến trúc

Quy tắc về tầng, phụ thuộc và ranh giới giữa ba module. Kiến trúc ở đây không phải sơ đồ đẹp, mà là những luật máy kiểm tra được để code không rối dần theo thời gian.

Cập nhật 12/09/2026kiến trúcmodule

Kiến trúc để làm gì

Kiến trúc tồn tại để trả lời hai câu:

  1. Code mới này đặt ở đâu? Không có câu trả lời rõ thì mỗi người đặt một chỗ, và sáu tháng sau không ai tìm được gì.
  2. Đổi chỗ này thì vỡ chỗ nào? Không trả lời được thì mỗi thay đổi đều là canh bạc, và đội sẽ sợ sửa code — đó là lúc dự án bắt đầu chết.

Sơ đồ kiến trúc không kiểm tra được thì sẽ lạc hậu sau ba tháng. Nên mọi luật dưới đây đều được viết ở dạng máy kiểm tra được, và được gắn vào quality gate tự động.

Bốn tầng

┌──────────────────────────────────────────┐
│ Giao diện        màn hình, component     │
├──────────────────────────────────────────┤
│ Nghiệp vụ        quy tắc, máy trạng thái │
├──────────────────────────────────────────┤
│ Dữ liệu          kho, nguồn, bộ nhớ đệm  │
├──────────────────────────────────────────┤
│ Hạ tầng          mạng, lưu trữ, thiết bị │
└──────────────────────────────────────────┘

Luật phụ thuộc: chỉ được gọi xuống, không được gọi lên.

TầngĐược phép biếtCấm biết
Giao diệnNghiệp vụChi tiết mạng, chi tiết database
Nghiệp vụGiao diện của tầng dữ liệuFramework giao diện, thư viện HTTP cụ thể
Dữ liệuHạ tầngBất kỳ thứ gì ở tầng nghiệp vụ hay giao diện
Hạ tầngKhông gì cảToàn bộ phần trên

Điểm quan trọng nhất: tầng nghiệp vụ không được phụ thuộc vào framework. Quy tắc tính tiền, quy tắc chuyển trạng thái task, quy tắc thứ tự tin nhắn — những thứ này phải test được mà không cần khởi động app, không cần mạng, không cần màn hình. Đó cũng là điều kiện để test ở tầng rẻ nhất, tức là shift left.

Ranh giới giữa ba module

   ┌──────────┐  ┌──────────┐  ┌──────────┐
   │   Chat   │  │   Task   │  │ Bán hàng │
   └────┬─────┘  └────┬─────┘  └────┬─────┘
        └─────────────┼─────────────┘
                ┌─────┴──────┐
                │ Dùng chung │
                └────────────┘

Luật:

  1. Module không gọi thẳng vào code nội bộ của module khác. Chat không import trực tiếp một hàm bên trong Task.
  2. Muốn dùng của nhau thì qua giao diện công khai đã khai báo của module đó, hoặc qua sự kiện.
  3. Tầng dùng chung không được biết về module nào cả. Nếu code dùng chung có câu if (module === 'chat') thì nó không thuộc về tầng dùng chung.
  4. Không tạo phụ thuộc vòng. Chat dùng Task mà Task cũng dùng Chat thì hai module đó thực chất là một, và phải tách lại cho đúng.

Bốn luật này được kiểm tra tự động bằng công cụ phân tích phụ thuộc, chạy ở tầng PR. Vi phạm là cổng đỏ, không phải là lời nhắc.

Năm điểm chạm giữa các module

Đây là những chỗ ba module thật sự phải nói chuyện với nhau. Ngoài năm chỗ này, mọi kết nối mới đều cần một quyết định được ghi lại:

Điểm chạmCách nốiChủ sở hữu
Danh tính và phân quyền người dùngDịch vụ dùng chungDùng chung
Thông báo đẩyHàng đợi sự kiệnDùng chung
Đính kèm tệpDịch vụ dùng chungDùng chung
Tạo task từ một tin nhắnSự kiện, một chiềuTask nhận
Gắn hội thoại vào một đơn hàngTham chiếu định danh, không nhúng dữ liệuBán hàng giữ

Hai điểm cuối đi một chiều và chỉ truyền định danh, không truyền cả khối dữ liệu. Truyền định danh thì mỗi bên vẫn tự làm chủ dữ liệu của mình; nhúng dữ liệu thì hai bên sẽ lệch nhau và không ai biết bên nào đúng.

Quy tắc cho code dùng chung

Đưa một đoạn code lên tầng dùng chung là quyết định khó đảo ngược, nên:

  • Chỉ đưa lên khi đã có ít nhất hai module thật sự dùng. Một chỗ dùng thì để tại chỗ. Trừu tượng hoá sớm tốn kém hơn nhân đôi code.
  • Code dùng chung cần 2 approve, một trong đó là chủ kỹ thuật.
  • Thay đổi phá vỡ tương thích phải có giai đoạn chuyển đổi: thêm cái mới, chuyển dần, rồi mới bỏ cái cũ. Không đổi thẳng.
  • Mọi nhánh logic phải có test.

Dữ liệu

  • Mỗi bảng dữ liệu có đúng một module làm chủ. Module khác đọc qua giao diện công khai, không truy vấn thẳng vào bảng của người khác.
  • Mọi thay đổi cấu trúc dữ liệu đi kèm migration có thể lùi được. Migration không lùi được thì không qua cổng.
  • Migration phải chạy được trên dữ liệu thật đã có, không chỉ trên database rỗng. Test điều này trước khi merge, không phải lúc release.
  • Dữ liệu trên máy người dùng phải có số phiên bản schema, và app phải nâng cấp được từ mọi phiên bản còn được hỗ trợ.

Khi nào được phá luật

Luật kiến trúc có thể phá, nhưng phải cố ý và có ghi chép:

  1. Ghi một quyết định theo mẫu ghi quyết định, nói rõ luật nào bị phá và vì sao.
  2. Chủ kỹ thuật duyệt.
  3. Thêm ngoại lệ vào cấu hình công cụ kiểm tra phụ thuộc, kèm bình luận trỏ về quyết định đó.
  4. Ghi vào danh sách nợ kỹ thuật kèm điều kiện để dọn.

Phá luật mà không làm bốn bước trên thì không phải quyết định kiến trúc, chỉ là code lỗi chưa bị bắt.