Làm cách nào để bạn quản lý cơ sở mã cơ bản cho một API được tạo phiên bản?


104

Tôi đã đọc về các chiến lược lập phiên bản cho các API ReST và điều mà không ai trong số họ đề cập đến là cách bạn quản lý cơ sở mã cơ bản.

Giả sử chúng tôi đang thực hiện một loạt các thay đổi đột phá đối với một API - ví dụ: thay đổi tài nguyên Khách hàng của chúng tôi để nó trả về các trường forenamesurnametrường riêng biệt thay vì một nametrường duy nhất . (Đối với ví dụ này, tôi sẽ sử dụng giải pháp lập phiên bản URL vì rất dễ hiểu các khái niệm liên quan, nhưng câu hỏi này cũng có thể áp dụng cho thương lượng nội dung hoặc tiêu đề HTTP tùy chỉnh)

Bây giờ chúng ta có một điểm cuối tại http://api.mycompany.com/v1/customers/{id}và một điểm cuối không tương thích khác tại http://api.mycompany.com/v2/customers/{id}. Chúng tôi vẫn đang phát hành các bản sửa lỗi và cập nhật bảo mật cho API v1, nhưng việc phát triển tính năng mới hiện đang tập trung vào v2. Làm cách nào để chúng tôi viết, kiểm tra và triển khai các thay đổi đối với máy chủ API của mình? Tôi có thể thấy ít nhất hai giải pháp:

  • Sử dụng nhánh / thẻ kiểm soát nguồn cho cơ sở mã v1. v1 và v2 được phát triển và triển khai độc lập, với các hợp nhất kiểm soát sửa đổi được sử dụng khi cần thiết để áp dụng cùng một bản sửa lỗi cho cả hai phiên bản - tương tự như cách bạn quản lý cơ sở mã cho các ứng dụng gốc khi phát triển một phiên bản mới trong khi vẫn hỗ trợ phiên bản trước.

  • Làm cho cơ sở mã tự nhận biết các phiên bản API, vì vậy bạn sẽ có một cơ sở mã duy nhất bao gồm cả đại diện khách hàng v1 và đại diện khách hàng v2. Xử lý việc lập phiên bản như một phần của kiến ​​trúc giải pháp của bạn thay vì vấn đề triển khai - có thể sử dụng một số kết hợp không gian tên và định tuyến để đảm bảo các yêu cầu được xử lý bởi phiên bản chính xác.

Ưu điểm rõ ràng của mô hình chi nhánh là việc xóa các phiên bản API cũ là rất dễ dàng - chỉ cần dừng triển khai chi nhánh / thẻ thích hợp - nhưng nếu bạn đang chạy một số phiên bản, bạn có thể kết thúc với một cấu trúc chi nhánh và đường ống triển khai thực sự phức tạp. Mô hình "cơ sở mã hợp nhất" tránh được vấn đề này, nhưng (tôi nghĩ?) Sẽ khiến việc xóa các tài nguyên và điểm cuối không dùng nữa khỏi cơ sở mã sẽ khó hơn nhiều khi chúng không còn được yêu cầu. Tôi biết điều này có thể là chủ quan vì không có câu trả lời chính xác đơn giản, nhưng tôi tò mò muốn hiểu cách các tổ chức duy trì các API phức tạp trên nhiều phiên bản đang giải quyết vấn đề này.


41
Cảm ơn vì đã đặt câu hỏi này! Tôi không thể tin rằng nhiều người không trả lời câu hỏi này !! Tôi phát ngán và mệt mỏi khi mọi người đều có ý kiến ​​về cách các phiên bản đi vào hệ thống, nhưng dường như không ai giải quyết vấn đề khó thực sự là gửi các phiên bản đến mã thích hợp của họ. Đến giờ chắc hẳn đã có ít nhất một loạt các "mẫu" hoặc "giải pháp" được chấp nhận cho vấn đề dường như phổ biến này. Đặt ra một số câu hỏi điên rồ về SO liên quan đến "lập phiên bản API". Quyết định cách chấp nhận các phiên bản là FRIKKIN ĐƠN GIẢN (tương đối)! Xử lý nó trong codebase khi nó vào được, là CỨNG!
arijeet

Câu trả lời:


45

Tôi đã sử dụng cả hai chiến lược mà bạn đề cập. Trong số hai cách đó, tôi ủng hộ cách tiếp cận thứ hai, đơn giản hơn, trong các trường hợp sử dụng hỗ trợ nó. Nghĩa là, nếu nhu cầu lập phiên bản đơn giản, thì hãy thiết kế phần mềm đơn giản hơn:

  • Số lượng thay đổi thấp, thay đổi phức tạp thấp hoặc lịch trình thay đổi tần suất thấp
  • Những thay đổi phần lớn trực giao với phần còn lại của cơ sở mã: API công khai có thể tồn tại hòa bình với phần còn lại của ngăn xếp mà không yêu cầu phân nhánh "quá mức" (đối với bất kỳ định nghĩa nào về thuật ngữ đó bạn chọn áp dụng) phân nhánh trong mã

Tôi không thấy quá khó khi xóa các phiên bản không dùng nữa bằng mô hình này:

  • Phạm vi kiểm tra tốt có nghĩa là tách ra một API đã ngừng hoạt động và mã hỗ trợ liên quan đảm bảo không có (tốt, tối thiểu) hồi quy
  • Chiến lược đặt tên tốt (tên gói có phiên bản API hoặc phiên bản API xấu hơn một chút trong tên phương thức) giúp dễ dàng tìm mã có liên quan
  • Mối quan tâm xuyên suốt khó hơn; các sửa đổi đối với hệ thống phụ trợ cốt lõi để hỗ trợ nhiều API phải được cân nhắc rất cẩn thận. Tại một số điểm, chi phí của chương trình phụ trợ phiên bản (Xem nhận xét về "quá mức" ở trên) lớn hơn lợi ích của một cơ sở mã duy nhất.

Cách tiếp cận đầu tiên chắc chắn đơn giản hơn từ quan điểm giảm xung đột giữa các phiên bản cùng tồn tại, nhưng chi phí duy trì các hệ thống riêng biệt có xu hướng lớn hơn lợi ích của việc giảm xung đột phiên bản. Điều đó nói rằng, thật đơn giản để tạo một ngăn xếp API công khai mới và bắt đầu lặp lại trên một nhánh API riêng biệt. Tất nhiên, sự mất mát thế hệ xảy ra gần như ngay lập tức, và các nhánh biến thành một mớ hỗn độn của việc hợp nhất, hợp nhất các giải pháp xung đột, và những trò vui khác.

Cách tiếp cận thứ ba là ở lớp kiến ​​trúc: áp dụng một biến thể của mẫu Mặt tiền và trừu tượng hóa các API của bạn thành các lớp được tạo phiên bản đối mặt công khai nói chuyện với cá thể Mặt tiền thích hợp, đến lượt nó sẽ nói chuyện với phần phụ trợ thông qua bộ API của chính nó. Mặt tiền của bạn (tôi đã sử dụng Bộ điều hợp trong dự án trước của mình) trở thành gói riêng của nó, độc lập và có thể kiểm tra được, đồng thời cho phép bạn di chuyển các API giao diện người dùng độc lập với phần phụ trợ và lẫn nhau.

Điều này sẽ hoạt động nếu các phiên bản API của bạn có xu hướng hiển thị các loại tài nguyên giống nhau, nhưng có các biểu diễn cấu trúc khác nhau, như trong ví dụ tên đầy đủ / ngoại hối / họ của bạn. Sẽ khó hơn một chút nếu họ bắt đầu dựa vào các tính toán phụ trợ khác nhau, như trong "Dịch vụ phụ trợ của tôi đã trả về lãi kép được tính toán không chính xác đã được hiển thị trong API công khai v1. Khách hàng của chúng tôi đã vá hành vi không chính xác này. Do đó, tôi không thể cập nhật điều đó tính toán trong chương trình phụ trợ và áp dụng cho đến v2. Do đó, bây giờ chúng tôi cần phân nhánh mã tính toán lãi suất của mình. " May mắn thay, những điều đó có xu hướng không thường xuyên: thực tế mà nói, người tiêu dùng RESTful API ưa thích các biểu diễn tài nguyên chính xác hơn khả năng tương thích ngược của bug-for-bug, ngay cả trong số các thay đổi không đột ngột trên một GETnguồn tài nguyên lý tưởng.

Tôi sẽ quan tâm đến quyết định cuối cùng của bạn.


5
Chỉ tò mò, trong mã nguồn, bạn có sao chép các mô hình giữa v0 và v1 không thay đổi? Hoặc bạn có v1 sử dụng một số mô hình v0? Đối với tôi, tôi sẽ bối rối nếu tôi thấy v1 sử dụng mô hình v0 cho một số trường. Nhưng mặt khác, nó sẽ làm giảm sự cồng kềnh của mã. Để xử lý nhiều phiên bản, chúng ta chỉ phải chấp nhận và sống với mã trùng lặp cho các mô hình không bao giờ thay đổi?
EdgeCaseBerg

1
Hồi ức của tôi là các mô hình được phiên bản mã nguồn của chúng tôi độc lập với chính API, vì vậy, ví dụ: API v1 có thể sử dụng Mô hình V1 và API v2 cũng có thể sử dụng Mô hình V1. Về cơ bản, biểu đồ phụ thuộc nội bộ cho API công khai bao gồm cả mã API được tiếp xúc, cũng như mã "thực hiện" phụ trợ như mã máy chủ và mã mô hình. Đối với nhiều phiên bản, chiến lược duy nhất mà tôi từng sử dụng là sao chép toàn bộ ngăn xếp - một cách tiếp cận kết hợp (mô-đun A được sao chép, mô-đun B được tạo phiên bản ...) có vẻ rất khó hiểu. Tất nhiên là YMMV. :)
Palpatim

2
Tôi không chắc mình làm theo những gì được đề xuất cho cách tiếp cận thứ ba. Có bất kỳ ví dụ công khai nào về mã có cấu trúc giống như nó không?
Ehtesh Choudhury

13

Đối với tôi cách tiếp cận thứ hai là tốt hơn. Tôi đã sử dụng nó cho các dịch vụ web SOAP và cũng có kế hoạch sử dụng nó cho REST.

Khi bạn viết, cơ sở mã phải nhận thức được phiên bản, nhưng một lớp tương thích có thể được sử dụng làm lớp riêng biệt. Trong ví dụ của bạn, cơ sở mã có thể tạo ra biểu diễn tài nguyên (JSON hoặc XML) với họ và tên, nhưng lớp tương thích sẽ thay đổi nó thành chỉ có tên.

Cơ sở mã chỉ nên triển khai phiên bản mới nhất, giả sử v3. Lớp tương thích sẽ chuyển đổi các yêu cầu và phản hồi giữa phiên bản mới nhất v3 và các phiên bản được hỗ trợ, ví dụ v1 và v2. Lớp tương thích có thể có một bộ điều hợp riêng biệt cho từng phiên bản được hỗ trợ, có thể được kết nối dưới dạng chuỗi.

Ví dụ:

Yêu cầu ứng dụng khách v1: v1 thích ứng với v2 ---> v2 thích ứng với v3 ----> codebase

Yêu cầu ứng dụng khách v2: v1 thích ứng với v2 (bỏ qua) ---> v2 thích ứng với v3 ----> codebase

Đối với phản hồi, bộ điều hợp hoạt động theo hướng ngược lại. Nếu bạn đang sử dụng Java EE, bạn có thể chuỗi bộ lọc servlet làm chuỗi bộ điều hợp chẳng hạn.

Xóa một phiên bản rất dễ dàng, xóa bộ điều hợp tương ứng và mã kiểm tra.


Rất khó để đảm bảo tính tương thích nếu toàn bộ cơ sở mã bên dưới đã thay đổi. An toàn hơn nhiều khi giữ lại cơ sở mã cũ cho các bản phát hành sửa lỗi.
Marcelo Cantos

5

Việc phân nhánh có vẻ tốt hơn nhiều đối với tôi và tôi đã sử dụng cách tiếp cận này trong trường hợp của mình.

Đúng như bạn đã đề cập - sửa lỗi backporting sẽ đòi hỏi một số nỗ lực, nhưng đồng thời hỗ trợ nhiều phiên bản dưới một cơ sở nguồn (với định tuyến và tất cả các nội dung khác) sẽ yêu cầu bạn nếu không muốn nói là ít hơn, nhưng ít nhất cũng phải nỗ lực, làm cho hệ thống nhiều hơn phức tạp và quái dị với các nhánh logic khác nhau bên trong (tại một số thời điểm lập phiên bản, bạn chắc chắn sẽ đi đến việc case()trỏ rất lớn đến các mô-đun phiên bản có mã bị trùng lặp hoặc thậm chí còn tệ hơn if(version == 2) then...). Cũng đừng quên rằng vì mục đích hồi quy, bạn vẫn phải giữ cho các thử nghiệm phân nhánh.

Về chính sách lập phiên bản: tôi sẽ giữ nguyên tối đa -2 phiên bản so với phiên bản hiện tại, không hỗ trợ cho những phiên bản cũ - điều này sẽ tạo động lực cho người dùng di chuyển.


Tôi đang nghĩ đến việc thử nghiệm trong một cơ sở mã duy nhất vào lúc này. Bạn đã đề cập rằng các bài kiểm tra sẽ luôn cần được phân nhánh nhưng tôi nghĩ rằng tất cả các bài kiểm tra cho v1, v2, v3, v.v. cũng có thể sống trong cùng một giải pháp và tất cả đều được chạy cùng một lúc. Tôi đang nghĩ đến việc trang trí các cuộc thử nghiệm với các thuộc tính trong đó quy định những gì các phiên bản hỗ trợ họ: ví dụ [Version(From="v1", To="v2")], [Version(From="v2", To="v3")], [Version(From="v1")] // All versions Chỉ cần khám phá nó bây giờ, bao giờ nghe bất cứ ai làm điều đó?
Lee Gunn

1
Chà, sau 3 năm, tôi đã học được rằng không có câu trả lời chính xác cho câu hỏi ban đầu: D. Nó rất phụ thuộc vào dự án. Nếu bạn có đủ khả năng đóng băng API và chỉ duy trì nó (ví dụ: sửa lỗi) thì tôi vẫn sẽ phân nhánh / tách mã liên quan (logic nghiệp vụ liên quan đến API + các bài kiểm tra + điểm cuối còn lại) và có tất cả nội dung được chia sẻ trong thư viện riêng biệt (với các bài kiểm tra riêng của nó ). Nếu V1 sẽ cùng tồn tại với V2 trong một thời gian khá lâu và công việc tính năng vẫn đang diễn ra thì tôi sẽ giữ chúng lại với nhau và thử nghiệm nữa (bao gồm V1, V2, v.v. và đặt tên cho phù hợp).
edmarisov

1
Cảm ơn. Vâng, nó có vẻ là một không gian khá cố chấp. Trước tiên, tôi sẽ thử cách tiếp cận một giải pháp và xem nó diễn ra như thế nào.
Lee Gunn

0

Thông thường, việc giới thiệu một phiên bản chính của API dẫn bạn đến tình huống phải duy trì nhiều phiên bản là một sự kiện không (hoặc không nên) xảy ra rất thường xuyên. Tuy nhiên, không thể tránh khỏi hoàn toàn. Tôi nghĩ về tổng thể, đó là một giả định an toàn rằng một phiên bản chính, sau khi được giới thiệu, sẽ là phiên bản mới nhất trong một khoảng thời gian tương đối dài. Dựa trên điều này, tôi muốn đạt được sự đơn giản trong mã với chi phí trùng lặp vì nó giúp tôi tự tin hơn về việc không phá vỡ phiên bản trước khi tôi giới thiệu các thay đổi trong phiên bản mới nhất.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.