我們都喜歡擁有閃亮的新工具,但討厭不斷更新它們的苦差事。這適用於任何事物:作業系統、應用程式、API、Linux 軟體包。當我們的程式碼因為更新而停止工作時,這是痛苦的,而當更新甚至不是我們發起的時,痛苦會加倍。
在 Web API 開發中,您始終面臨著每次新更新都會破壞使用者程式碼的風險。如果你的產品是API,那麼每次這些更新都會令人恐懼。 Monite 的主要產品是我們的 API 和白標 SDK。我們是一家 API 優先的公司,因此我們非常注意保持我們的 API 穩定且易於使用。因此,重大變更的問題幾乎是我們優先考慮的問題。
常見的解決方案是向您的客戶發出棄用警告並很少發布重大變更。突然之間,您的發布現在可能需要幾個月的時間,並且某些功能必須保持隱藏甚至不合併,直到每個下一個版本為止。這會減慢您的開發速度並迫使您的用戶每隔幾個月更新一次整合。
如果您加快發布速度,您的用戶將不得不過於頻繁地更新他們的整合。如果延長發布之間的時間,公司的進度就會變慢。你帶給用戶的越不方便,對你來說就越方便,反之亦然。這當然不是最佳方案。我們希望按照自己的步調前進,而不會對現有客戶造成任何破壞,而常規棄用方法是不可能做到這一點的。這就是我們選擇替代方案的原因:API 版本控制。
這是一個非常簡單的想法:隨時發布任何重大更改,但將它們隱藏在新的 API 版本下。它為您提供了兩全其美的優勢:用戶的整合不會經常被破壞,並且您將能夠以您喜歡的任何速度移動。用戶可以隨時遷移——沒有任何壓力。
考慮到這個想法的簡單性,它對於任何公司來說都是完美的。這就是您期望在典型的工程部落格中讀到的內容。遺憾的是,事情沒那麼簡單。
API 版本控制非常困難。一旦你開始實施它,它虛幻的簡單性很快就會消失。遺憾的是,互聯網從未真正警告過您,因為有關該主題的資源少得驚人。他們中的絕大多數人爭論 API 版本應該放在哪裡,但只有少數文章試圖回答:「我們如何實作它?」。最常見的是:
單獨的部署可能會變得非常昂貴且難以支持,複製單一路由不能很好地擴展到大型更改,並且複製整個應用程式會創建大量額外的程式碼,以至於在幾個版本之後您就會開始淹沒其中。
即使您嘗試選擇最便宜的,版本控制的負擔很快就會趕上。一開始,它會感覺很簡單:在這裡添加另一個模式,在那裡添加業務邏輯的另一個分支,並在最後複製一些路由。但是,如果版本足夠多,您的業務邏輯將很快變得難以管理,許多開發人員會弄錯應用程式版本和API 版本,並開始對資料庫中的資料進行版本控制,您的應用程式將變得無法維護。
您可能希望永遠不會同時擁有超過兩個或三個 API 版本;您每隔幾個月就可以刪除舊版本。如果您只支持少數內部消費者,確實如此。但您組織以外的客戶不會享受每隔幾個月就被迫升級的體驗。
API 版本控制很快就會成為基礎架構中最昂貴的部分之一,因此提前進行深入研究至關重要。如果您只支援內部消費者,那麼使用 GraphQL 等工具可能會更簡單,但它很快就會變得與版本控制一樣昂貴。
如果您是一家新創公司,明智的做法是將 API 版本控制推遲到開發的後期階段,此時您有資源來正確執行此操作。在那之前,棄用和附加變更策略可能就足夠了。您的 API 並不總是看起來很棒,但至少您可以透過避免明確版本控制來節省大量資金。
經過幾次嘗試和許多錯誤後,我們正處於十字路口:我們上面提到的先前版本控制方法的維護成本太高。經過我們的努力,我設計了以下完美版本控制框架所需的要求清單:
遺憾的是,我們現有的方法幾乎沒有其他選擇。這時我想到了一個瘋狂的想法:如果我們嘗試建立一些複雜的、適合工作的東西——例如 Stripe 的 API 版本控制,會怎麼樣?
經過無數次實驗,我們現在擁有了 Cadwyn:一個開源 API 版本控制框架,它不僅實作了 Stripe 的方法,而且顯著地建構於其之上。我們將討論它的 Fastapi 和 Pydantic 實現,但核心原則與語言和框架無關。
所有其他版本控制方法的問題是我們重複太多。當我們的合約只有一小部分被破壞時,為什麼我們要複製整個路由、控制器甚至應用程式?
借助 Cadwyn,每當 API 維護人員需要建立新版本時,他們都會將重大變更應用於最新的架構、模型和業務邏輯。然後他們創建一個版本變更——一個封裝新版本和先前版本之間所有差異的類別。
例如,假設以前我們的客戶可以建立一個具有地址的用戶,但現在我們希望允許他們指定多個地址而不是單一地址。版本變更如下圖所示:
class ChangeUserAddressToAList(VersionChange): description = ( "Renamed `User.address` to `User.addresses` and " "changed its type to an array of strings" ) instructions_to_migrate_to_previous_version = ( schema(User).field("addresses").didnt_exist, schema(User).field("address").existed_as(type=str), ) @convert_request_to_next_version_for(UserCreateRequest) def change_address_to_multiple_items(request): request.body["addresses"] = [request.body.pop("address")] @convert_response_to_previous_version_for(UserResource) def change_addresses_to_single_item(response): response.body["address"] = response.body.pop("addresses")[0]
Cadwyn 使用instructions_to_migrate_to_previous_version 為較舊的API 版本的模式產生程式碼,這兩個轉換器函數是讓我們能夠維護任意數量版本的技巧。過程如下圖所示:
我們的 API 維護人員創建版本更改後,他們需要將其添加到我們的 VersionBundle 中,以告訴 Cadwyn 此 VersionChange 將包含在某個版本中:
VersionBundle( Version( date(2023, 4, 27), ChangeUserAddressToAList ), Version( date(2023, 4, 12), CollapseUserAvatarInfoIntoAnID, MakeUserSurnameRequired, ), Version(date(2023, 3, 15)), )
就是這樣:我們新增了一項重大更改,但我們的業務邏輯僅處理單一版本 - 最新版本。即使我們新增了數十個 API 版本,我們的業務邏輯仍然不受版本控制邏輯、不斷重命名、if 和資料轉換器的影響。
版本變更取決於 API 的公共接口,我們幾乎從不向現有 API 版本添加重大變更。這意味著一旦我們發布了該版本,它就不會被破壞。
因為版本更改描述了版本內的重大更改,並且舊版本中沒有重大更改,所以我們可以確定我們的版本更改是完全不可變的 - 它們永遠不會有理由更改。不可變實體比業務邏輯的一部分更容易維護,因為它總是在發展。版本變更也會被一個接一個地應用——在版本之間形成一系列轉換器,可以將任何請求遷移到任何較新版本,並將任何回應遷移到任何舊版本。
API 合約比模式和欄位複雜得多。它們由所有端點、狀態代碼、錯誤、錯誤訊息,甚至是業務邏輯行為所組成。 Cadwyn 使用我們上面描述的相同 DSL 來處理端點和狀態代碼,但錯誤和業務邏輯行為是另一回事:它們不可能使用 DSL 來描述,它們需要嵌入到業務邏輯中。
This makes such version changes much more expensive to maintain than all others because they affect business logic. We call this property a "side effect" and we try to avoid them at all costs because of their maintenance burden. All version changes that want to modify business logic will need to be marked as having side effects. It will serve as a way to know which version changes are "dangerous":
class RequireCompanyAttachedForPayment(VersionChangeWithSideEffects): description = ( "User must now have a company_id in their account " "if they want to make new payments" )
It will also allow API maintainers to check that the client request uses an API version that includes this side effect:
if RequireCompanyToBeAttachedForPayment.is_applied: validate_company_id_is_attached(user)
Cadwyn has many benefits: It greatly reduces the burden on our developers and can be integrated into our infrastructure to automatically generate the changelog and improve our API docs.
However, the burden of versioning still exists and even a sophisticated framework is not a silver bullet. We do our best to only use API versioning when absolutely necessary. We also try to make our API correct on the first try by having a special "API Council". All significant API changes are reviewed there by our best developers, testers, and tech writers before any implementation gets moving.
Special thanks to Brandur Leach for his API versioning article at Stripe and for the help he extended to me when I implemented Cadwyn: it would not be possible without his help.
以上是Monite 的 API 版本控制的詳細內容。更多資訊請關注PHP中文網其他相關文章!