構建啞巴,智能重構:如何從 Ruby on Rails 代碼中解決問題
已發表: 2022-03-11有時,客戶向我們提出我們真的不喜歡的功能請求。 並不是我們不喜歡我們的客戶——我們愛我們的客戶。 並不是我們不喜歡這個功能——大多數客戶要求的功能都與他們的業務目標和收入完全一致。
有時,我們不喜歡功能請求,因為解決它的最簡單方法是編寫糟糕的代碼,而且我們沒有一個優雅的解決方案。 這將使我們許多 Rails 開發人員通過 RubyToolbox、GitHub、開發人員博客和 StackOverflow 進行毫無結果的搜索,尋找可以讓我們自我感覺更好的 gem、插件或示例代碼。
重構 Ruby on Rails 代碼
好吧,我在這裡告訴你:編寫糟糕的代碼是可以的。 有時,糟糕的 Rails 代碼比在時間緊迫的情況下實施的考慮不周的解決方案更容易重構為漂亮的代碼。
這是我從可怕的創可貼解決方案中解決問題時喜歡遵循的 Rails 重構過程:
從另一個角度來看,這是一個已逐步重構的功能的 Git 提交日誌:
這是另一篇來自 Toptal 網絡同事的關於大規模重構的有趣文章。
讓我們看看它是如何完成的。
觀點
步驟 1.從視圖開始
假設我們正在為一項新功能開票。 客戶告訴我們: “訪客應該能夠在歡迎頁面上查看活動項目列表。”
這張票需要一個可見的改變,所以一個合理的開始工作的地方應該是在視圖中。 這個問題很簡單,而且我們都接受過多次訓練來解決這個問題。 我將以錯誤的方式解決它,並演示如何將我的解決方案重構到適當的區域。 解決問題錯誤的方法可以幫助我們克服不知道正確解決方案的困境。
首先,假設我們有一個名為Project
的模型,它有一個名為active
的布爾屬性。 我們想要獲取所有active
等於true
的Projects
的列表,因此我們可以使用Project.where(active: true)
,並使用each
塊對其進行循環。
app/views/pages/welcome.haml: %ul.projects - Project.where(active: true).each do |project| %li.project= link_to project_path(project), project.name
我知道你在說什麼:“那永遠不會通過代碼審查”或“我的客戶肯定會因此解僱我。” 是的,這個解決方案打破了模型-視圖-控制器的關注點分離,它可能會導致難以追踪的雜散數據庫調用,並且將來可能會變得難以維護。 但請考慮這樣做的價值The Wrong Way :
- 您可以在 15 分鐘內完成暫存更改。
- 如果留在裡面,這個塊很容易被緩存。
- 解決這個 Rails 問題很簡單(可以交給初級開發人員)。
步驟 2.部分
做了The Wrong Way之後,我對自己感覺不好,想隔離我的壞代碼。 如果這種變化顯然只是視圖層的一個問題,我可以將我的恥辱重構為部分。
app/views/pages/welcome.haml: = render :partial => 'shared/projects_list' app/views/shared/projects_list.haml: %ul.projects - Project.where(active: true).each do |project| %li.project= link_to project_path(project), project.name
這樣好一點。 顯然,我們仍然犯了在視圖中查詢模型的錯誤,但至少當維護者稍後進來並看到我可怕的部分時,他們將有一種直接的方法來解決特別是該 Rails 代碼問題。 修復一些明顯愚蠢的東西總是比修復一個實現不佳、有缺陷的抽象更容易。
步驟 3.助手
Rails 中的助手是一種為視圖的一部分創建 DSL(領域特定語言)的方法。 您必須使用 content_tag 而不是 haml 或 HTML 來重寫您的代碼,但您會獲得允許操作數據結構的好處,而不會讓其他開發人員對 15 行非打印視圖代碼盯著您。
如果我在這裡使用助手,我可能會重構出li
標籤。
app/views/shared/projects_list.haml: %ul.projects - Project.where(active: true).each do |project| = project_list_item(project) app/helpers/projects_helper.rb: def project_list_item(project) content_tag(:li, :class => 'project') do link_to project_path(project), project.name end end
控制器
步驟 4.將其移至控制器
也許你糟糕的代碼不僅僅是一個視圖問題。 如果您的代碼仍然有異味,請查找可以從視圖轉換到控制器的查詢。
app/views/shared/projects_list.haml: %ul.projects - @projects_list.each do |project| = project_list_item(project) app/controllers/pages_controller.rb: def welcome @projects = Project.where(active: true) end
步驟 5.控制器過濾器

將代碼移動到控制器before_filter
或after_filter
的最明顯原因是您在多個控制器操作中復制的代碼。 如果您想將控制器操作的目的與視圖的要求分開,您還可以將代碼移動到控制器過濾器中。
app/controllers/pages_controller.rb: before_filter :projects_list def welcome end def projects_list @projects = Project.where(active:true) end
步驟 6.應用程序控制器
假設你需要你的代碼出現在每個頁面上,或者你想讓控制器輔助函數對所有控制器都可用,你可以將你的函數移動到 ApplicationController 中。 如果更改是全局的,您可能還需要修改應用程序佈局。
app/controllers/pages_controller.rb: def welcome end app/views/layouts/application.haml: %ul.projects - projects_list.each do |project| = project_list_item(project) app/controllers/application_controller.rb: before_filter :projects_list def projects_list @projects = Project.where(active: true) end
模型
步驟 7.重構模型
正如 MVC 的座右銘所說:胖模型、瘦控制器和啞視圖。 我們被期望將我們所能做的一切重構到模型中,而且確實最複雜的功能最終將成為模型關聯和模型方法。 我們應該始終避免在模型中進行格式化/查看事情,但是將數據轉換為其他類型的數據是允許的。 在這種情況下,重構模型的最佳方法是where(active: true)
子句,我們可以將其轉換為範圍。 使用範圍很有價值,不僅因為它使調用看起來更漂亮,而且如果我們決定添加像delete
或outdated
這樣的屬性,我們可以修改這個範圍,而不是尋找所有的where
子句。
app/controllers/application_controller.rb: before_filter :projects_list def projects_list @projects = Project.active end app/models/project.rb: scope :active, where(active: true)
步驟 8.模型過濾器
在這種情況下,我們對模型的before_save
或after_save filters
沒有特別的用途,但我通常採取的下一步是將功能從控制器和模型方法轉移到模型過濾器中。
假設我們有另一個屬性num_views
。 如果num_views > 50
,則項目變為非活動狀態。 我們可以在 View 中解決這個問題,但是在 View 中進行數據庫更改是不合適的。 我們可以在 Controller 中解決它,但是我們的 Controller 應該盡可能的薄! 我們可以在模型中輕鬆解決它。
app/models/project.rb: before_save :deactivate_if_over_num_views def deactivate_if_over_num_views if num_views > 50 self.active = false fi end
注意:您應該避免在模型過濾器中調用self.save
,因為這會導致遞歸保存事件,並且應用程序的數據庫操作層無論如何都應該是控制器。
步驟 9.庫
有時,您的功能足夠大,可以保證它擁有自己的庫。 您可能希望將它移動到庫文件中,因為它在很多地方被重用,或者它足夠大,您想單獨對其進行開發。
將庫文件存儲在lib/目錄中很好,但隨著它們的增長,您可以將它們轉移到真正的 RubyGem 中! 將代碼移動到庫中的一個主要優點是您可以將庫與模型分開測試。
無論如何,在項目列表的情況下,我們可以證明將scope :active
調用從Project
模型移動到庫文件中,並將其帶回 Ruby:
app/models/project.rb: class Project < ActiveRecord::Base include Activeable before_filter :deactivate_if_over_num_views end lib/activeable.rb: module Activeable def self.included(k) k.scope :active, k.where(active: true) end def deactivate_if_over_num_views if num_views > 50 self.active = false end end end
注意: self.included
方法在 Rails 模型類被加載並作為變量k
傳入類範圍時被調用。
結論
在這個 Ruby on Rails 重構教程中,我們用了不到 15 分鐘的時間實現了一個解決方案,並將其放在舞台上供用戶測試,準備好被特性集接受或刪除。 在重構過程結束時,我們有一段代碼列出了一個框架,用於在多個模型中實現可列出、可激活的項目,這些項目甚至可以通過最嚴格的審查過程。
在您自己的 Rails 重構過程中,如果您有信心跳過管道中的幾個步驟(例如,從視圖跳轉到控制器,或從控制器跳轉到模型),請隨意跳過管道。 請記住,代碼流是從視圖到模型。
不要害怕看起來很愚蠢。 現代語言與舊的 CGI 模板渲染應用程序的區別並不在於我們每次都以正確的方式做每一件事——而是我們花時間重構、重用和分享我們的努力。