构建哑巴,智能重构:如何从 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 模板渲染应用程序的区别并不在于我们每次都以正确的方式做每一件事——而是我们花时间重构、重用和分享我们的努力。