Categories
程式開發

從Ruby移植到TypeScript後,我們的API問題解決了!


我們之前將React前端從JavaScript移植到了TypeScript,但將後端仍使用Ruby。最近,我們也將後端移植到了TypeScript。

本文最初發佈於execute program官方博客,經原作者授權由InfoQ中文站翻譯並分享。

使用Ruby後端,我們有時會忘記一個特定的API屬性包含一個字符串數組,而不是一個字符串。有時我們更改了一個在多個地方引用的API,但卻忘記了更新其中一個地方。在測試覆蓋率不到100%的系統中,這些都是動態語言常見的問題。 (即使覆蓋率百分之百,也仍然會發生這種情況;只是可能性小些。)

然而,在前端移植到TypeScript之後,這類問題就消失了。我後端開發經驗更豐富些,但我在後端犯的低級錯誤比在前端多。這說明移植後端是一個好主意。

在2019年3月,大約兩個週的時間內,我將後端從Ruby移植到了TypeScript。很順利!我們在2019年4月14日將其投入生產,當時處於封閉測試階段。沒出什麼問題;用戶都沒有註意到。這是後端移植準備以及剛剛移植之後的時間線:

從Ruby移植到TypeScript後,我們的API問題解決了! 1

在移植期間,我編寫了大量的自定義基礎設施。我們有一個200行的自定義測試執行器;120行的自定義數據庫模型庫;一個更大的、橫跨前後端代碼的API路由庫。

在我們的自定義基礎設施中,路由是最值得討論的部分。它封裝了Express,增強了在客戶端和服務器代碼之間共享的API類型。這意味著,當API的一端發生變化時,另一端甚至無法編譯,直到更新到相互匹配為止。

這是博文列表的後端處理程序,是系統中最簡單的一個:

router.handleGet(api.blog, async () => {
  return {
    posts: blog.posts,
  }
})

如果我們將posts鍵重命名為blogPosts,我們將得到下面這行編譯錯誤。 (簡單起見,我在這裡刪除了錯誤消息中實際的對像類型。)

Property 'posts' is missing in type '...' but required in type '...'.

每個端點由一個api.someNameHere對象定義,它在客戶端和服務器之間共享。注意,處理程序定義沒有直接命名任何類型;它們都是從api.blogIndex參數推斷出來的。
這既適用於像blog這樣的簡單端點,也適用於復雜端點。例如,我們的課程API端點有一個很深的鍵.lesson.steps[index].isInteractive,它是一個布爾值。現在,下面所有這些錯誤都不可能出現了:

  • 如果我們試圖在客戶端訪問isinteractive或從服務器返回它,則無法編譯;必須是isInteractive,大寫的“I”。
  • 如果對於isInteractive,服務器返回一個數值,則無法編譯。
  • 如果客戶端將isInteractive存儲在類型為number的變量中,則無法編譯。
  • 如果我們修改API定義,將isInteractive由布爾型改為數值型,那麼客戶端和服務器都無法編譯,直到它們被修復。

這些都不涉及代碼生成;這是用io-ts和幾百行自定義路由代碼完成的。

定義這些API類型有一些工作量,但並不困難。當修改API的結構時,我們必須知道結構是如何修改的。我們將我們的理解寫在API類型定義中,然後編譯器向我們顯示所有需要修改的地方。

你得使用一段時間之後,才能意識到它的價值。我們可以在API中將大型子對像從一個地方移到另一個地方,重命名它們的鍵,將一個對象分割成兩個獨立的對象,將多個對象合併為一個新對象,或者分割和合併整個端點,而不用擔心我們在客戶端或服務器中是否遺漏了相應的變化。

這是一個真實的例子。最近,我在四個週末,花了大約20個小時,重新設計了Execute Program的API。整個API的結構都發生了變化,API、服務器和客戶端的差異代碼總計達數万行。我重新設計了服務器端的路由定義代碼(如上面的handleGet);重寫了API的所有類型定義,對其中許多API的結構進行了大幅的更改;重寫了客戶端調用API的每一部分。在這個過程中,292個源文件中有246個被修改。

在整個重新設計的過程中,我只依賴於類型系統。在這20個小時的最後一個小時裡,我開始運行測試,大部分都通過了。最後,我們做了一次完整的手工測試,發現了三個小Bug。

這三個Bug都是邏輯錯誤:條件語句意外出錯,而類型系統通常無法檢測到此類錯誤。錯誤在幾分鐘內就解決了。重新設計的API是在幾個月前完成部署的,所以這篇文章就是它提供的(以及Execute Program的其他所有內容)。

(這並不是說,靜態類型系統可以保證我們的代碼總是正確的,或者保證我們不需要測試。但是重構變得容易多了。我們將在下一篇文章中討論更大的測試問題。)

有一個地方我們使用了代碼生成:我們使用schemats從數據庫結構生成類型定義。它連接到Postgres數據庫,查看列的類型,並將相應的TypeScript類型定義轉儲為普通的“.d.ts”文件,供應用程序的其餘部分使用。

每次運行遷移時,遷移腳本都會重新生成數據庫模式類型文件,因此,我們不必對這些類型做任何手工維護。數據庫模型使用數據庫類型定義來確保應用程序代碼可以正確地訪問數據庫的每個部分。沒有丟失的表;沒有丟失的列;沒有向不可為空的列中存入空值;不要忘記在可為空的列中處理null;所有這些都是在編譯時靜態驗證的。

所有這些一起創建了一個完整的靜態類型鏈,從數據庫一直到前端React props:

  • 如果數據庫列的類型發生變化,其他服務器端代碼(如API處理程序)將無法編譯,直到所有東西都更新到與之相匹配。
  • 如果服務器端API處理程序與客戶端API消費者不匹配,則一個或兩個都無法編譯。
  • 如果客戶端React組件與來自API的數據不匹配,它們將無法編譯。

自完成這次遷移之後,就再也沒有出現過任何API不匹配卻通過編譯的情況。我們再也沒有因為API雙方在數據格式上的差異而導致生產環境失敗。這不是因為自動化測試;我們不為API本身編寫任何測試。

有這些保證非常棒:我們可以專注於應用程序中重要的部分!我花在爭論類型上的時間非常少——遠遠少於我花在追踪那些透過Ruby或JavaScript代碼層傳播的令人困惑的錯誤上的時間,這些錯誤在遠離錯誤原始來源的地方導致了令人困惑的異常。

下面是我們後端移植的開發時間線。為了評估結果,我們花了很多時間,並寫了很多新代碼:

從Ruby移植到TypeScript後,我們的API問題解決了! 2

對於類似本文這樣的文章,有一個常見的反對意見我們還沒有討論:難道通過編寫測試就不能得到相同的結果嗎?絕對不能,我們將在下一篇文章中討論!

原文鏈接:

Porting to TypeScript Solved Our API Woes