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.
Nội dung bài
Kiến trúc để làm gì
Kiến trúc tồn tại để trả lời hai câu:
- 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ì.
- Đổ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ết | Cấm biết |
|---|---|---|
| Giao diện | Nghiệp vụ | Chi tiết mạng, chi tiết database |
| Nghiệp vụ | Giao diện của tầng dữ liệu | Framework giao diện, thư viện HTTP cụ thể |
| Dữ liệu | Hạ tầng | Bất kỳ thứ gì ở tầng nghiệp vụ hay giao diện |
| Hạ tầng | Khô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:
- 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.
- 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.
- 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. - 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ạm | Cách nối | Chủ sở hữu |
|---|---|---|
| Danh tính và phân quyền người dùng | Dịch vụ dùng chung | Dùng chung |
| Thông báo đẩy | Hàng đợi sự kiện | Dùng chung |
| Đính kèm tệp | Dịch vụ dùng chung | Dùng chung |
| Tạo task từ một tin nhắn | Sự kiện, một chiều | Task nhận |
| Gắn hội thoại vào một đơn hàng | Tham chiếu định danh, không nhúng dữ liệu | Bá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:
- 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.
- Chủ kỹ thuật duyệt.
- 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 đó.
- 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.

