Interface Contracts Tập 5: Versioning & Những sai lầm chí mạng. Khi hệ thống phải thay đổi, làm sao để nâng cấp API mà không làm chết các app cũ?
Trải qua 4 tập, chúng ta đã xây dựng được những "bản hợp đồng" hoàn hảo, tách biệt các thành phần, và có cả hệ thống tự động kiểm tra (Contract Testing). Nhưng có một sự thật phũ phàng trong ngành phần mềm: Yêu cầu kinh doanh luôn luôn thay đổi.
Hôm nay bạn thiết kế API trả về firstName và lastName. Ngày mai sếp yêu cầu gộp chung thành một trường fullName. Nếu bạn sửa thẳng tay vào API hiện tại, các ứng dụng Mobile cũ (chưa kịp update trên App Store) lập tức sụp đổ. Vậy làm sao để nâng cấp hợp đồng mà không gây ra thảm họa?
1. Nguyên tắc vàng: Tương thích ngược (Backward Compatibility)
Giống như trong đời thực, bạn không thể tự ý tước bỏ quyền lợi của một bên đã ký hợp đồng mà không báo trước. Bạn chỉ có thể thêm quyền lợi mới. Trong thiết kế API, điều này gọi là giữ tính tương thích ngược.
Quy tắc "Sinh tồn" khi sửa đổi Contract:
| Thao tác | Cho phép? | Lý do |
|---|---|---|
| Thêm trường dữ liệu mới (Optional) | ✅ Có | Client cũ không biết đến trường này nên chúng sẽ bỏ qua. Client mới thì có dữ liệu để dùng. |
| Thêm Endpoint mới | ✅ Có | Hoàn toàn an toàn, không ảnh hưởng hệ thống cũ. |
| Xóa một trường dữ liệu | ❌ Không | Client cũ đang gọi trường đó, xóa đi sẽ gây ra lỗi NullPointerException hoặc undefined. |
Đổi tên trường (VD: id -> userId) |
❌ Không | Tương tự như xóa, Client cũ không tìm thấy trường id nữa. |
| Thêm trường mới (Bắt buộc/Required) | ❌ Không | Nếu thêm bắt buộc ở Request, các Client cũ không gửi trường này lên -> Request lập tức bị 400 Bad Request. |
Giải pháp an toàn: Khi muốn đổi
firstNamevàlastNamethànhfullName, hãy giữ nguyên hai trường cũ và thêm trườngfullNamevào. Các app cũ vẫn lấy được tên, app mới thì dùng trường mới. Dữ liệu có thể hơi dư thừa một chút, nhưng hệ thống an toàn tuyệt đối.
2. Các chiến lược đánh phiên bản (Versioning)
Khi những thay đổi quá lớn và vi phạm quy tắc tương thích ngược, bạn buộc phải "ký một hợp đồng mới" hoàn toàn. Đó là lúc chúng ta dùng Versioning.
Cách 1: URI Versioning (Phổ biến nhất)
Đây là cách rõ ràng và dễ tiếp cận nhất. Phiên bản được gắn thẳng vào URL.
GET /api/v1/users(Dành cho app cũ)GET /api/v2/users(Dành cho app mới, có sự thay đổi cấu trúc dữ liệu)- Ưu điểm: Dễ test trên trình duyệt, phân định rõ rành bằng Route ở Backend.
Cách 2: Header Versioning (Gọn gàng nhất)
URL giữ nguyên độ trong sáng, phiên bản được định nghĩa qua Accept header.
- URL:
GET /api/users - Header (Cũ):
Accept: application/vnd.mycompany.v1+json - Header (Mới):
Accept: application/vnd.mycompany.v2+json - Ưu điểm: Code Backend giữ chuẩn RESTful thuần túy, không bị rác URL.
Quy trình "Khai tử" (Deprecation)
Bạn không thể giữ code của v1 vĩnh viễn vì chi phí bảo trì rất cao. Quy trình chuẩn để khai tử một hợp đồng cũ là:
- Đánh dấu API
v1là@Deprecated(trên Swagger) để không ai dùng cho tính năng mới. - Trả về HTTP Header:
Warning: 299 - "API v1 is deprecated and will be removed in 6 months." - Theo dõi log xem lượng traffic vào
v1giảm xuống mức an toàn (ví dụ < 1%) mới tiến hành xóa bỏ.
3. Những "sai lầm chí mạng" (Anti-patterns) cần tránh
Dù đã nắm triết lý, nhiều lập trình viên vẫn mắc phải các lỗi thiết kế hợp đồng sau:
Lỗi #1: Rò rỉ chi tiết ẩn (Leaky Abstraction)
Hợp đồng không được phép để lộ "bí mật kinh doanh" của bên cung cấp.
- Sai lầm: API trả về thẳng Object của Database (
Entity), làm lộ ra các trường nhưpasswordHash,deletedAt, hoặc trả về mã lỗi SQLORA-12154. - Khắc phục: Luôn có một lớp ánh xạ dữ liệu (Data Transfer Object - DTO). Database Entity là việc của Backend, DTO là thứ duy nhất Client nhìn thấy.
Lỗi #2: Hợp đồng quá "béo" (Vi phạm Interface Segregation Principle)
Một Interface ôm đồm quá nhiều việc, ép Client phải phụ thuộc vào những thứ họ không cần.
- Sai lầm: Có một interface
IUserbắt Client phải implement cảregister(),login(),updateProfile(),banUser(). - Khắc phục: Xé nhỏ hợp đồng.
banUser()nên thuộc vềIAdminActions. Đừng bắt app dành cho User thường phải chứa hợp đồng liên quan đến Admin.
Lỗi #3: Thiếu định nghĩa lỗi (Unhappy Paths)
Lập trình viên thường chỉ thiết kế hợp đồng cho lúc "Trời quang mây tạnh" (Success 200) mà quên mất lúc bão bùng.
- Sai lầm: Swagger chỉ có định nghĩa Response
200. Khi lỗi, hệ thống quăng ra một dòng text hoặc một cấu trúc JSON lộn xộn không thể lường trước. - Khắc phục: Hợp đồng lỗi phải nghiêm ngặt không kém hợp đồng thành công. Tiêu chuẩn hóa một
ErrorResponsechung (ví dụ RFC 7807) chứaerrorCode,message,details.
Tổng kết Series: Sức mạnh của Interface Contracts
Qua 5 tập, chúng ta đã chứng kiến sự chuyển mình của tư duy thiết kế:
- Bên trong Code: Hợp đồng bảo vệ logic và dữ liệu khỏi trạng thái rác (Pre/Post-conditions, Invariants).
- Trong Kiến trúc (SOLID): Hợp đồng tách rời các module, giúp cắm/rút Database, UI dễ như lắp Lego (Dependency Inversion).
- Trong Microservices: Hợp đồng (Swagger/gRPC) là ngôn ngữ chung giúp hàng chục team làm việc song song không giẫm chân lên nhau.
- Bảo vệ Hệ thống: Contract Testing (Pact) là tấm khiên tự động chặn đứng những thay đổi phá hoại.
- Tiến hóa (Evolution): Versioning giúp hệ thống chuyển mình, lớn lên từng ngày mà không làm tổn thương những giá trị cũ.
Thiết kế phần mềm giỏi không nằm ở việc viết thuật toán cho chạy nhanh hơn 1 mili-giây, mà là ở việc vạch ra những đường ranh giới giao tiếp rõ ràng. Khi hợp đồng đã chuẩn, mọi sự hỗn loạn của dự án sẽ tự khắc lùi bước!
All Rights Reserved