Buggy Rails 代碼:Rails 開發人員最常犯的 10 個錯誤
已發表: 2022-03-11Ruby on Rails(“Rails”)是一種流行的開源框架,基於 Ruby 編程語言,致力於簡化和簡化 Web 應用程序開發過程。
Rails 建立在約定優於配置的原則之上。 簡而言之,這意味著,默認情況下,Rails 假定其專家開發人員將遵循“標準”最佳實踐約定(例如命名、代碼結構等),如果您這樣做,一切都會為您“自動-magically”,而您無需指定這些詳細信息。 雖然這種範式有其優點,但也並非沒有缺陷。 最值得注意的是,框架中發生在幕後的“魔法”有時會導致假裝、混亂和“到底發生了什麼?” 類型的問題。 它還可能對安全性和性能產生不良影響。
因此,儘管 Rails 易於使用,但它也不難誤用。 本教程著眼於 10 個常見的 Rails 問題,包括如何避免它們以及它們導致的問題。
常見錯誤 #1:在控制器中放置太多邏輯
Rails 基於 MVC 架構。 在 Rails 社區中,我們一直在談論胖模型、瘦控制器,但我繼承的幾個最近的 Rails 應用程序違反了這個原則。 將視圖邏輯(最好放在幫助程序中)或域/模型邏輯移動到控制器中太容易了。
問題是控制器對象將開始違反單一職責原則,這使得未來對代碼庫的更改變得困難且容易出錯。 通常,您的控制器中應該有的唯一邏輯類型是:
- 會話和 cookie 處理。 這可能還包括身份驗證/授權或您需要執行的任何其他 cookie 處理。
- 型號選擇。 給定從請求傳入的參數,查找正確模型對象的邏輯。 理想情況下,這應該是對單個 find 方法的調用,該方法設置一個實例變量,以便稍後用於呈現響應。
- 請求參數管理。 收集請求參數並調用適當的模型方法來持久化它們。
- 渲染/重定向。 呈現結果(html、xml、json 等)或重定向,視情況而定。
雖然這仍然突破了單一職責原則的限制,但它是 Rails 框架要求我們在控制器中擁有的最低限度。
常見錯誤 #2:在視圖中放置太多邏輯
開箱即用的 Rails 模板引擎 ERB 是構建具有可變內容的頁面的好方法。 但是,如果您不小心,您很快就會得到一個混合了 HTML 和 Ruby 代碼的大文件,可能難以管理和維護。 這也是一個可能導致大量重複的領域,從而導致違反 DRY(不要重複自己)原則。
這可以通過多種方式表現出來。 一是在視圖中過度使用條件邏輯。 作為一個簡單的例子,考慮一個可用的current_user
方法返回當前登錄用戶的情況。 通常,視圖文件中最終會出現這樣的條件邏輯結構:
<h3> Welcome, <% if current_user %> <%= current_user.name %> <% else %> Guest <% end %> </h3>
處理此類事情的更好方法是確保current_user
返回的對象始終設置,無論是否有人登錄,並且它以合理的方式回答視圖中使用的方法(有時稱為 null目的)。 例如,您可以像這樣在app/controllers/application_controller
中定義current_user
助手:
require 'ostruct' helper_method :current_user def current_user @current_user ||= User.find session[:user_id] if session[:user_id] if @current_user @current_user else OpenStruct.new(name: 'Guest') end end
然後,這將使您能夠用以下簡單的代碼行替換之前的視圖代碼示例:
<h3>Welcome, <%= current_user.name -%></h3>
幾個額外推薦的 Rails 最佳實踐:
- 適當地使用視圖佈局和局部視圖來封裝頁面上重複的內容。
- 使用 Draper gem 之類的演示者/裝飾器將視圖構建邏輯封裝在 Ruby 對像中。 然後,您可以將方法添加到此對像中以執行您可能已放入視圖代碼中的邏輯操作。
常見錯誤#3:在模型中加入太多邏輯
鑑於最小化視圖和控制器中的邏輯的指導,MVC 架構中唯一可以放置所有邏輯的地方就是模型,對吧?
嗯,不完全是。
許多 Rails 開發人員實際上犯了這個錯誤,最終將所有內容都粘在他們的ActiveRecord
模型類中,從而導致 mongo 文件不僅違反了單一職責原則,而且是維護的噩夢。
諸如生成電子郵件通知、與外部服務接口、轉換為其他數據格式等功能與 ActiveRecord 模型的核心職責沒有太大關係, ActiveRecord
模型應該只是在數據庫中查找和保存數據。
所以如果邏輯不應該放在視圖中,也不應該放在控制器中,也不應該放在模型中,那麼,它應該放在哪裡呢?
輸入普通的舊 Ruby 對象 (PORO)。 對於像 Rails 這樣的綜合框架,新開發人員通常不願意在框架之外創建自己的類。 然而,將邏輯從模型中移到 PORO 中通常正是醫生為了避免過於復雜的模型而要求的。 使用 PORO,您可以將電子郵件通知或 API 交互等內容封裝到它們自己的類中,而不是將它們粘貼到ActiveRecord
模型中。
因此,考慮到這一點,一般來說,應該保留在模型中的唯一邏輯是:
-
ActiveRecord
配置(即關係和驗證) - 簡單的變異方法來封裝更新少數屬性並將它們保存在數據庫中
- 訪問包裝器以隱藏內部模型信息(例如,結合數據庫中的
first_name
和last_name
字段的full_name
方法) - 複雜的查詢(即比簡單的
find
更複雜); 一般來說,您永遠不應該在模型類本身之外使用where
方法或任何其他類似的查詢構建方法
常見錯誤 #4:使用通用輔助類作為垃圾場
這個錯誤實際上是上面錯誤#3的必然結果。 如前所述,Rails 框架強調 MVC 框架的命名組件(即模型、視圖和控制器)。 對於屬於這些組件的類中的事物種類有相當好的定義,但有時我們可能需要似乎不適合這三者中的任何一個的方法。
Rails 生成器方便地構建一個幫助器目錄和一個新的幫助器類,以配合我們創建的每個新資源。 然而,開始將任何不適合模型、視圖或控制器的功能填充到這些幫助類中變得非常誘人。
儘管 Rails 肯定是以 MVC 為中心的,但沒有什麼能阻止您創建自己的類類型並添加適當的目錄來保存這些類的代碼。 當您有其他功能時,請考慮將哪些方法組合在一起,並為包含這些方法的類找到合適的名稱。 使用像 Rails 這樣的綜合框架並不是放棄良好的面向對象設計最佳實踐的藉口。
常見錯誤 #5:使用太多寶石
Ruby 和 Rails 由豐富的 gem 生態系統支持,這些 gem 共同提供了開發人員能想到的任何功能。 這對於快速構建複雜的應用程序非常有用,但我也看到許多臃腫的應用程序,與提供的功能相比,應用程序的Gemfile
中的 gem 數量大得不成比例。
這會導致幾個 Rails 問題。 過度使用 gem 會使 Rails 進程的大小超出其需要的大小。 這會降低生產性能。 除了讓用戶感到沮喪之外,這還可能導致需要更大的服務器內存配置並增加運營成本。 啟動更大的 Rails 應用程序也需要更長的時間,這會使開發速度變慢,並使自動化測試花費更長的時間(通常,慢速測試根本不會經常運行)。
請記住,您帶入應用程序的每個 gem 可能又依賴於其他 gem,而這些 gem 又可能依賴於其他 gem,依此類推。 因此,添加其他寶石可以產生復合效果。 例如,添加rails_admin
gem 總共會增加 11 個 gem,比基礎 Rails 安裝增加了 10% 以上。
在撰寫本文時,全新的 Rails 4.1.0 安裝在Gemfile.lock
文件中包含 43 個 gem。 這顯然比Gemfile
中包含的要多,它代表了少數標準 Rails gem 作為依賴項引入的所有 gem。
在添加每個 gem 時,請仔細考慮額外的開銷是否值得。 例如,開發人員通常會隨意添加rails_admin
gem,因為它本質上為模型結構提供了一個不錯的 Web 前端,但它實際上只不過是一個花哨的數據庫瀏覽工具。 即使您的應用程序需要具有額外權限的管理員用戶,您也可能不希望給他們原始數據庫訪問權限,與添加此 gem 相比,開發您自己的更簡化的管理功能會更好地為您服務。
常見錯誤 #6:忽略您的日誌文件
雖然大多數 Rails 開發人員都知道在開發和生產過程中可用的默認日誌文件,但他們通常沒有足夠注意這些文件中的信息。 雖然許多應用程序在生產中依賴於 Honeybadger 或 New Relic 等日誌監控工具,但在整個開發和測試應用程序的過程中密切關注您的日誌文件也很重要。
正如本教程前面提到的,Rails 框架為您做了很多“魔法”,尤其是在模型中。 在模型中定義關聯可以很容易地拉入關係並使所有內容都可用於您的視圖。 為您生成填充模型對象所需的所有 SQL。 那太棒了。 但是你怎麼知道生成的 SQL 是有效的呢?
您經常遇到的一個示例稱為 N+1 查詢問題。 雖然這個問題很好理解,但觀察它發生的唯一真正方法是查看日誌文件中的 SQL 查詢。
例如,您在一個典型的博客應用程序中有以下查詢,您將在其中顯示一組選定帖子的所有評論:

def comments_for_top_three_posts posts = Post.limit(3) posts.flat_map do |post| post.comments.to_a end end
當我們查看調用此方法的請求的日誌文件時,我們會看到類似以下內容,其中進行了一次查詢以獲取三個 post 對象,然後再進行三個查詢以獲取每個對象的評論:
Started GET "/posts/some_comments" for 127.0.0.1 at 2014-05-20 20:05:13 -0700 Processing by PostsController#some_comments as HTML Post Load (0.4ms) SELECT "posts".* FROM "posts" LIMIT 3 Comment Load (5.6ms) ELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 1]] Comment Load (0.4ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 2]] Comment Load (1.5ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 3]] Rendered posts/some_comments.html.erb within layouts/application (12.5ms) Completed 200 OK in 581ms (Views: 225.8ms | ActiveRecord: 10.0ms)
Rails 中ActiveRecord
的快速加載功能可以通過讓您提前指定將要加載的所有關聯來顯著減少查詢的數量。 這是通過在正在構建的 Arel ( ActiveRecord::Relation
) 對像上調用includes
(或preload
) 方法來完成的。 使用includes
, ActiveRecord
可確保使用盡可能少的查詢來加載所有指定的關聯; 例如:
def comments_for_top_three_posts posts = Post.includes(:comments).limit(3) posts.flat_map do |post| post.comments.to_a end end
執行上述修改後的代碼時,我們在日誌文件中看到所有評論都收集在一個查詢中,而不是三個:
Started GET "/posts/some_comments" for 127.0.0.1 at 2014-05-20 20:05:18 -0700 Processing by PostsController#some_comments as HTML Post Load (0.5ms) SELECT "posts".* FROM "posts" LIMIT 3 Comment Load (4.4ms) SELECT "comments".* FROM "comments" WHERE"comments "."post_id" IN (1, 2, 3) Rendered posts/some_comments.html.erb within layouts/application (12.2ms) Completed 200 OK in 560ms (Views: 219.3ms | ActiveRecord: 5.0ms)
效率更高。
這個 N+1 問題的解決方案實際上只是作為示例,如果您沒有給予足夠的關注,那麼您的應用程序中可能存在“幕後”的低效率。 這裡的要點是,您應該在開發期間檢查您的開發和測試日誌文件,以檢查(並解決!)構建響應的代碼中的低效率。
查看日誌文件是發現代碼效率低下並在應用程序投入生產之前糾正它們的好方法。 否則,在您的系統上線之前,您可能不會意識到由此產生的 Rails 性能問題,因為您在開發和測試中使用的數據集可能比在生產中小得多。 如果您正在開發一個新應用程序,即使您的生產數據集一開始也可能很小,並且您的應用程序看起來運行良好。 但是,隨著您的生產數據集的增長,像這樣的 Rails 問題會導致您的應用程序運行得越來越慢。
如果你發現你的日誌文件被一堆你不需要的信息堵塞了,你可以做一些事情來清理它們(那裡的技術適用於開發和生產日誌)。
常見錯誤 #7:缺乏自動化測試
Ruby 和 Rails 默認提供強大的自動化測試功能。 許多 Rails 開發人員使用 TDD 和 BDD 樣式編寫非常複雜的測試,並使用更強大的測試框架以及 rspec 和 cucumber 之類的 gem。
儘管向您的 Rails 應用程序添加自動化測試是多麼容易,但我對我繼承或加入的項目中實際上沒有編寫測試(或充其量很少)的情況感到非常不愉快開發團隊。 雖然有很多關於你的測試應該有多全面的爭論,但很明顯,每個應用程序至少應該存在一些自動化測試。
作為一般經驗法則,應該為控制器中的每個操作編寫至少一個高級集成測試。 在未來的某個時候,其他 Rails 開發人員很可能想要擴展或修改代碼,或者升級 Ruby 或 Rails 版本,而這個測試框架將為他們提供一種清晰的方式來驗證應用程序的基本功能是否正確在職的。 這種方法的另一個好處是它為未來的開發人員提供了對應用程序提供的完整功能集合的清晰描述。
常見錯誤 #8:阻止對外部服務的調用
Rails 服務的第 3 方提供商通常可以通過封裝其 API 的 gem 輕鬆地將他們的服務集成到您的應用程序中。 但是,如果您的外部服務中斷或開始運行非常緩慢,會發生什麼情況?
為了避免阻塞這些調用,而不是在正常處理請求期間直接在 Rails 應用程序中調用這些服務,您應該在可行的情況下將它們移至某種後台作業隊列服務。 Rails 應用程序中用於此目的的一些流行 gem 包括:
- 延誤工作
- 雷斯克
- 西德基克
如果將處理委派給後台作業隊列是不切實際或不可行的,那麼您需要確保您的應用程序具有足夠的錯誤處理和故障轉移規定,以應對外部服務出現故障或遇到問題時不可避免的情況. 您還應該在沒有外部服務的情況下測試您的應用程序(可能通過從網絡中刪除您的應用程序所在的服務器),以驗證它不會導致任何意外後果。
常見錯誤 #9:與現有的數據庫遷移結婚
Rails 的數據庫遷移機制允許您創建指令來自動添加和刪除數據庫表和行。 由於包含這些遷移的文件是按順序命名的,因此您可以從頭開始回放它們,以將空數據庫帶入與生產相同的模式。 因此,這是管理應用程序數據庫模式的精細更改並避免 Rails 問題的好方法。
雖然這在您的項目開始時確實很有效,但隨著時間的推移,數據庫創建過程可能會花費相當長的時間,有時遷移會放錯位置、亂序插入或從使用同一數據庫服務器的其他 Rails 應用程序引入。
Rails 在名為db/schema.rb
的文件(默認情況下)中創建當前模式的表示,該文件通常在運行數據庫遷移時更新。 通過運行rake db:schema:dump
任務,甚至可以在沒有遷移的情況下生成schema.rb
文件。 一個常見的 Rails 錯誤是檢查一個新的遷移到你的源代碼庫,而不是相應更新的schema.rb
文件。
當遷移失控並且運行時間過長,或者不再正確創建數據庫時,開發人員不應該害怕清除舊的遷移目錄,轉儲新的模式,然後從那裡繼續。 建立一個新的開發環境將需要rake db:schema:load
而不是大多數開發人員所依賴的rake db:migrate
。
Rails 指南中也討論了其中一些問題。
常見錯誤 #10:將敏感信息檢查到源代碼存儲庫中
Rails 框架使創建不受多種攻擊的安全應用程序變得容易。 其中一些是通過使用秘密令牌來保護與瀏覽器的會話來實現的。 儘管這個令牌現在存儲在config/secrets.yml
中,並且該文件從生產服務器的環境變量中讀取令牌,但過去的 Rails 版本將令牌包含在config/initializers/secret_token.rb
中。 該文件經常被錯誤地與您的應用程序的其餘部分一起簽入源代碼存儲庫,當這種情況發生時,任何有權訪問該存儲庫的人現在都可以輕鬆地危害您應用程序的所有用戶。
因此,您應該確保您的存儲庫配置文件(例如,用於 git 用戶的.gitignore
)排除帶有您的令牌的文件。 然後,您的生產服務器可以從環境變量或類似 dotenv gem 提供的機制中獲取它們的令牌。
教程總結
Rails 是一個強大的框架,它隱藏了構建強大的 Web 應用程序所需的許多醜陋的細節。 雖然這使 Rails Web 應用程序開發速度更快,但開發人員應注意潛在的設計和編碼錯誤,以確保他們的應用程序在發展過程中易於擴展和維護。
開發人員還需要注意可能使他們的應用程序更慢、更不可靠和更不安全的問題。 研究框架並確保您完全了解在整個開發過程中所做的架構、設計和編碼權衡非常重要,以幫助確保高質量和高性能的應用程序。