天天看點

來自 Rails 核心團隊成員的反對猴子更新檔的案例(譯文-來自:Shopify)

作者:閃念基因
來自 Rails 核心團隊成員的反對猴子更新檔的案例(譯文-來自:Shopify)

猴子更新檔(Monkey patching)被認為是 Ruby 程式設計語言更強大的功能之一。然而,在這篇文章的最後,我希望能說服你,即使使用它們,也應該謹慎使用,因為它們脆弱、危險,而且通常是不必要的。我還将分享有關如何在極少數情況下确實需要猴子更新檔的情況下盡可能安全地使用它們的技巧。

什麼是猴子更新檔(Monkey patching)?

如果您是 Ruby 新手,您可能不熟悉猴子修補(Monkey patching),因為其他一些語言很難更改現有代碼的行為。猴子更新檔(Monkey patching)是動态改變現有對象行為的代碼,通常是目前程式之外的對象。通常有猴子更新檔的應用程式正在改變 Rails 架構或其他 gem 的行為,但我也看到了猴子更新檔自己的應用程式,這不太有意義,因為猴子更新檔是全局的。當我說 monkey patch 時,我指的是比擴充現有類更廣泛的東西。在我看來,猴子更新檔是任何時候你以令人驚訝的方式改變底層庫的行為。讓我們看一個開源示例,以便更好地了解我的意思。

在 Shopify,我們維護着一個名為activerecord-pedant-adapter 的開源 gem 。這個擴充卡猴子修補了 Active Record 的 MySQL2Adapter 以改變和方法的行為。該更新檔確定如果啟用,資料庫連接配接将報告查詢警告。沒有這個更新檔,Rails 不支援報告 MySQL 查詢發出的警告。 executeexec_delete

在猴子更新檔方面,這個 gem 相對溫和,因為它調用super并且不改變 Active Record 執行的内部行為。但這仍然令人驚訝,因為該行為并非直接來自 Rails。

為了減少我們在 Rails 上的猴子更新檔,我們最近決定将行為上遊,這樣我們就可以存檔這個 gem 并幫助改進 Rails。感謝Shopify 的Adrianna 和Paarth 為實作這一上遊所做的工作。檢視拉取請求!對 Rails 的這一改變意味着我們可以從我們的代碼庫中删除這個猴子更新檔,并且離沒有更新檔更近了一步。

為什麼猴子更新檔是危險的?

雖然 monkey patching 非常強大并且為語言增加了大量的靈活性,但是能夠改變你所依賴的開源工具的行為是非常危險的。讓我們回顧一下猴子修補庫的一些後果。我們将使用修補 Rails 架構作為主要示例,因為修補主要架構比修補小 gem 風險更大。然而,大多數由猴子更新檔引起的問題都與任何類型的更新檔有關。

猴子更新檔使更新 Rails 和 Ruby 變得更加困難

更新 Rails 對于獲得新的架構功能、確定您的應用程式安全以及獲得最新的性能改進和錯誤修複至關重要。但是如果你給 Rails 打更新檔,你可能會把自己鎖在一個困難的或不可能的更新中。有時,由于 API 更改,猴子更新檔會在更新中中斷。Rails 隻提供對公共 API 的棄用,是以任何更改内部或私有 API 的猴子更新檔都不會收到棄用警告,您可能依賴于已更改或已完全删除的行為。當猴子更新檔位于應用程式所依賴的關鍵路徑時,這尤其有害。在其他情況下,猴子更新檔會阻止更新,因為您更改的路徑不再可行。弄清楚如何為最新的 Rails 重寫更新檔可能會非常乏味和耗時。

猴子更新檔可能會讓您容易受到安全問題的攻擊

猴子更新檔的缺點之一是它會讓你在安全釋出中容易受到攻擊。如果您正在修補後來發現存在漏洞的代碼,除非您進行與 Rails 相同的更改,否則您将得不到保護。猴子更新檔在實施後通常會被遺忘,是以除非有人知道代碼正在被打更新檔,否則即使您更新到最新的安全版本,它也可能使您的應用程式容易受到攻擊。

猴子更新檔增加了您的技術債務

通常會添加猴子更新檔,因為 Rails 沒有程式員想要的行為或無法快速修複應用程式中的錯誤。在大多數情況下,我看到的猴子更新檔都沒有得到很好的記錄或測試,留下了技術債務供以後清理。當更新檔存在時,很容易說“這裡和那裡還有什麼變化,這已經在為 Rails 打更新檔了”。這些更新檔繼續增長并變得越來越糟,使它們更難去除。這些通常出現在更新或生産事件中,沒有明确的删除路徑。

猴子更新檔意味着你不是一個好的開源公民

當您選擇猴子更新檔而不是發送更新檔和/或在上遊打開問題時,您就不是一個好的開源公民。使用猴子更新檔在内部修複錯誤意味着該錯誤僅在該代碼庫中得到修複。它不适用于您公司的其他 Rails 應用程式。它不會在您以後使用的 Rails 應用程式中修複。它沒有固定在架構中。向上遊送出修複或問題是為 Rails 社群做出貢獻并解決其他人可能也遇到的問題的好方法。

猴子更新檔可能會以令人驚訝的方式改變行為

雖然制作一個不會增加太多複雜性、太多變化或安全漏洞的最小猴子更新檔當然是可能的,但情況并非如此。Monkey 更新檔會全​局更改行為,适用于您正在修補的類或方法的所有調用者。這意味着猴子更新檔可以更改您在添加更新檔之前未識别的調用者的行為。

如果更新檔更改了不會導緻任何異常的行為,則可能很難追蹤到更新檔的來源。在之前的工作中,我們的代碼是猴子修補 Active Record 的查詢緩存。它不受版本保護,是以它強制應用程式使用類似于生命周期結束的 Rails 版本的查詢緩存行為,使其變得無用,因為它不知道我們有多個資料庫連接配接。我們花了數周的時間來弄清楚如何修複它,并最終從應用程式中删除了整個 gem。這隻是我舉出的猴子更新檔以令人驚訝的方式悄悄改變行為的衆多例子之一。

做什麼而不是猴子修補

從中得出的重要結論是,猴子修補是危險的、脆弱的,并且可以以您意想不到的方式改變行為。那麼你會怎麼做呢?作為開發人員,重要的是我們要深入研究什麼是壞的,為什麼壞的,以及如何在不使用猴子更新檔的情況下修複它。

在猴子修補之前,問問自己以下問題:

  • 優先更新會解決這個問題嗎?有時,如果我們在架構或庫中發現錯誤,它已經在上遊得到修複。在這種情況下,不要進行猴子修補,而是進行更新。
  • 真的有bug嗎?您可能錯誤地使用了一個庫,重新通路文檔可以幫助您了解它應該如何工作以避免以後可能導緻更大問題的更新檔。
  • 我可以在上遊解決這個問題嗎?我們應該将我們每天使用的開源工具視為我們應用程式的擴充,并在上遊修複錯誤,而不是猴子修補。

關于改進的說明

當我提到猴子更新檔有多危險時,我經常被問到我對使用改進有何看法 。精化是 Ruby 的一個特性,它允許您通過提供一種在本地而不是像猴子修補那樣全局擴充類的方法來修改程式。雖然它們包含更多内容,但我仍然認為不應該使用改進,絕對不應被視為“比猴子修補好得多”。

主要原因是改進很慢——真的很慢。在我們自己的 Shopify 代碼庫中,我們不再允許改進,因為我們發現它們正在減慢我們的 Rails 單體。在調查過程中,我們發現改進導緻啟動時間增加13 秒(!!),因為改進破壞了方法緩存。如果您在代碼庫的任何地方使用優化,Ruby VM 将變慢,因為它無法進行與不使用優化時相同的優化。

即使它們并不慢,改進也确實不比猴子更新檔好多少。如果我們檢視 monkey patching 不好的原因清單,大多數相同的問題都适用于改進。由細化更改的代碼仍然會中斷更新,導緻安全問題,并且它不會修複上遊代碼中存在的任何問題。

如果猴子更新檔很危險,為什麼還要經常使用它們?

在這一點上,我希望讓你相信猴子修補是危險的。您可能想知道,如果它很容易出錯,為什麼會如此頻繁地使用它。

我經常看到 Ruby 語言的愛好者在談論猴子更新檔有多麼強大,卻沒有解決它帶來的危險。在 Ruby 流行的早期,猴子更新檔被視為一種快速改變程式行為的方法。與不允許猴子修補或使其非常困​​難的語言相比,它被視為為程式提供了更多的自由和靈活性。

現在請不要誤會我的意思,我喜歡 Ruby,如果沒有 monkey patching,調試會更加困難,我們會陷入分叉庫或等待釋出的困境。當我們在沒有删除計劃的情況下将猴子更新檔部署到生産環境時,我的問題就來了。在我們等待釋出時,Monkey 更新檔應該用作臨時修複,而不是我們應用程式的永久修複。此外,這意味着我們永遠不應該修改我們擁有和控制的更新檔代碼。在我工作過的每個地方,我都看到團隊猴子修補公司擁有的寶石。這是完全沒有必要的,當我們這樣做時,它會将我們之前讨論的所有問題都引入到我們的應用程式中。

我認為猴子修補經常發生,因為它比上遊貢獻更容易。我明白了,首先你需要一個複制品,然後打開一個問題,然後希望維護者修複這個問題或合并你的拉取請求。我并不是建議我們等待無限的時間來解決我們無法控制的庫中的問題。然而,至少我們應該讓維護者知道存在問題,否則我們就有可能在我們的應用程式中一遍又一遍地遇到同樣的問題。

如果我必須使用猴子更新檔怎麼辦?

雖然如果我們從不猴子修補任何東西會很棒,但我們并不是生活在一個完美的世界中,是以如果你必須在 Rails 或任何其他 gem 中猴子修補行為,你可以遵循一些步驟來限制猴子修補的問題原因。幾年前,來自 GitHub 的 Cameron Dutro 寫了一篇關于Responsible Monkey Patching的精彩博文。檢查您是否對編寫不那麼危險的猴子更新檔的一些戰術方法感興趣。

當您絕對必須編寫猴子更新檔時,請遵循這些指南,以確定它們不那麼危險并且不會随着時間的推移成為更大的問題。

通知上遊維護者存在問題

在編寫你的更新檔之前,在上遊項目上打開一個問題以獲得維護者的回報。您正在處理的錯誤可能已在較新版本中修複,您隻需要更新即可。也有可能您錯誤地實作了該功能并更改您使用它的方式将避免該錯誤。最後,如果這是一個錯誤,維護人員将能夠為您确認這一點,并且您已確定有一份公開報告記錄了不正确的行為。

嘗試在上遊解決問題

在将猴子更新檔部署到您的應用程式之前,請向上遊發送一個更新檔。如果它被合并,即使你不能立即更新,你也會知道問題是在哪個版本中修複的。它還可以作為其他人如何處理錯誤的公共文檔。

如果您的更新檔被上遊接受,請確定将您的更新檔包裝在版本檢查中,以避免在上遊修複時使用它。這将防止留下不再有效或可能在以後導緻安全事件的更新檔。

将更新檔存儲在特定位置

如果你一定要寫猴子更新檔,最好把它放在一個目錄下,比如/lib/patches它是更新檔庫代碼的自文檔。避免在模型或控制器代碼或lib/. 這使得以後更容易找到要删除的猴子更新檔。它還有助于表明這是一個用于猴子更新檔的目錄并減少驚喜元素。

更新檔應盡可能小——盡可能使用繼承

在編寫猴子更新檔時,確定它隻打必要的更新檔。避免複制比你需要的更多的代碼——更新檔越小,它引起的問題就越少,也就越容易推理。

修補代碼時,盡量繼承原類,super盡可能使用。這将保護您不會錯過已更改的上遊行為。

徹底記錄猴子更新檔

将猴子更新檔添加到您的代碼庫時,應該對其進行詳細記錄。文檔應包括正在更改的内容和原因。它應該清楚地解釋正在改變的行為、錯誤是如何被發現的,并包括指向你在上遊打開的問題或拉取請求的連結。當那個猴子更新檔在生産中引起問題時,您将無法回答有關它的問題,這種可能性非零。關于 monkey patch 的良好文檔對于確定将來檢視它的任何人都清楚地了解其目的至關重要。

測試,并測試好

如果你猴子修補任何東西,那個更新檔應該被測試。這将幫助将來嘗試删除它的任何人了解問題是否已在上遊得到妥善解決。當上遊代碼被修複并且猴子更新檔被移除時,您編寫的測試應該仍然通過(API 更改的情況除外)。雖然文檔很有用,但測試是自我記錄的,并確定預期(和意外)的行為得到很好的了解。不測試猴子更新檔的行為可能意味着它修複的邊緣情況不明确,并在删除更新檔時導緻問題。

制定搬遷計劃

當我們修改更新檔代碼時,我們常常采用“編寫并忘記它”的方法。如果我們必須添加一個猴子更新檔,則需要有一個移除計劃,即使該計劃是在未來數年之後。添加更新檔後,在您的團隊倉庫中打開一個問題,這樣您就不會忘記稍後将其删除。在 Shopify,我們還有一個友善的“TODO”gem ,它會發送一個 slack 通知,提醒我們做某事。為您添加到代碼庫的任何猴子更新檔添加一個“TODO”是個好主意。

幫助!我的代碼庫已經充滿了更新檔!

如果您的代碼庫已經充滿了猴子更新檔,而您想删除它們,請不要擔心!你可以慢慢地燒掉更新檔,讓你的代碼庫到一個更好的地方。首先,找到所有更新檔并列出它們所在的位置。如果您的更新檔程式不在一個目錄中,這可能會很困難,但是當您找到它們時,将它們移動到您的更新檔程式目錄中,以便更容易找到它們。

确定哪些更新檔可以上遊到 Rails,哪些更新檔不再需要,因為錯誤已在上遊修複。一個接一個地打更新檔,最終你的代碼庫會更幹淨。這将需要您的工程團隊轉變思維方式,将猴子更新檔視為危險和需要避免的東西,而不是快速修複來自應用程式外部的任何問題。将 Rails 視為您的應用程式的擴充并感受到對它的所有權感,而不是将其視為“其他人的架構”,這對于使應用程式免于猴子更新檔至關重要。

擷取删除!

我希望這篇文章能說服你在未來避免使用猴子更新檔,并為你提供工具,讓你在必須絕對打更新檔的情況下編寫更好的猴子更新檔。在 Shopify,我們在 Rails main 上運作我們的主要整體。這樣做的好處之一是,當我們發現錯誤時,我們可以在上遊修複它,而不必添加猴子更新檔。我們永遠不必等待 Rails 的釋出,這讓我們可以自由地保持我們的代碼庫幹淨。

當然,我們仍然有我們仍在努力擺脫的遺留更新檔。那些遺留更新檔經常妨礙我們每周更新 Rails,是以這篇文章中的建議直接來自經驗。我希望這篇文章向您展示了 monkey patching 是多麼危險,它是如何浪費時間,導緻安全問題,并且通常隻會讓開發變得更加困難。我懇請您檢視您的代碼庫并确定應該删除的更新檔。向上遊發送 PR,并在 2023 年底使用比開始時更少的猴子更新檔。

真的沒有什麼比删除代碼更令人滿意的了,尤其是那些會導緻未來(和目前)問題的代碼。快樂删除!

作者:

Eileen Uchitelle是 Shopify 的 Ruby & Rails 基礎架構團隊的進階生産工程師,也是 Rails 核心團隊成員。在Twitter或GitHub上以@eileencodes 或 eileencodes.com找到她。

出處:https://shopify.engineering/the-case-against-monkey-patching

繼續閱讀