本文概述
丹尼尔·刘易斯(Daniel Lewis)从事专业的Ruby on Rails开发已有4年以上, 从事大约12个高流量的Web应用程序的开发, 其中许多是通过srcmini开发的。
有时, 客户会向我们提出我们真正不喜欢的功能请求。这并不是说我们不喜欢我们的客户, 而是我们爱我们的客户。这并不是说我们不喜欢该功能-大多数客户要求的功能都与他们的业务目标和收入完全一致。
有时, 我们不喜欢功能请求, 因为解决它的最简单方法是编写错误的代码, 而且我们没有一个优雅的解决方案。这将使我们许多Rails开发人员无法通过RubyToolbox, GitHub, 开发人员博客和StackOverflow进行无用的搜索, 以寻找让自己感觉更好的gem或插件或示例代码。
有时, 我们不喜欢功能请求, 因为解决它的最简单方法是编写错误的代码。
重构Ruby on Rails代码
好吧, 我在这里告诉你:编写错误的代码是可以的。有时, 不良的Rails代码比在时间紧迫的情况下实施的思想较差的解决方案更容易重构为精美的代码。
这是我在可怕的创可贴解决方案中按摩问题时遵循的Rails重构过程:
从另一个角度来看, 这是一步一步重构的功能的Git提交日志:
这是另一篇有关srcmini网络中的同事进行大规模重构的有趣文章。
让我们看看它是如何完成的。
观点
步骤1.从视图开始
假设我们正在开始一项新功能的入场券。客户告诉我们:”访问者应该能够在欢迎页面上查看活动项目的列表。”
此票证需要进行明显的更改, 因此可以在视图中找到一个合理的开始工作的地方。这个问题很简单, 而且我们都已经接受了多次解决的培训。我将解决”错误的方式”, 并演示如何将解决方案重构到适当的领域。解决问题错误的方法可以帮助我们克服不知道正确解决方案的麻烦。
首先, 假设我们有一个名为Project的模型, 其布尔属性为active。我们要获取活动等于true的所有项目的列表, 因此我们可以使用Project.where(active:true), 并在每个块上循环。
app/views/pages/welcome.haml:
%ul.projects
- Project.where(active: true).each do |project|
%li.project= link_to project_path(project), project.name
我知道你在说什么:”那将永远不会通过代码审查”或”我的客户一定会为此而开除我。”是的, 此解决方案打破了模型-视图-控制器的关注点分离, 可能导致难以跟踪的杂散数据库调用, 并且将来可能难以维护。但是, 考虑做”错误方式”的价值:
- 你可以在不到15分钟的时间内进行此更改。
- 如果保留, 则易于缓存。
- 解决此Rails问题很简单(可以提供给初级开发人员)。
步骤2.部分
完成”错误的方式”之后, 我对自己感到难过, 并想隔离我的错误代码。如果此更改显然仅是View层的关注点, 那么我可以将我的耻辱分解为一部分。
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
好一点了。显然, 我们仍然在View中犯了一个Model查询错误, 但是至少当维护者稍后进来并看到我的可怕部分时, 他们将特别有一种直接的方式来解决Rails代码问题。修复显然很愚蠢的东西总是比修复执行不佳的越野车抽象容易。
修复显然很愚蠢的东西总是比修复执行不佳的越野车抽象容易。
鸣叫
步骤3.帮手
Rails中的助手是一种为视图的一部分创建DSL(特定于域的语言)的方法。你必须使用content_tag而不是haml或HTML来重写代码, 但这样做的好处是, 你无需处理其他15行非打印的View代码, 就可以操纵数据结构而无需其他开发人员盯着你。
如果我在这里使用助手, 我可能会重构出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.将其移至控制器
也许你糟糕的代码不仅仅是View关注。如果你的代码仍然有问题, 请查找可以从视图过渡到控制器的查询。
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.控制器过滤器
将代码移至Controller的before_filter或after_filter的最明显原因是针对你在多个Controller动作中重复的代码。如果你想将Controller操作的目的与视图的需求分开, 也可以将代码移入Controller过滤器。
app/controllers/pages_controller.rb:
before_filter :projects_list
def welcome
end
def projects_list
@projects = Project.where(active:true)
end
步骤6.应用程序控制器
假定你需要在每个页面上显示代码, 或者要使Controller助手功能可用于所有控制器, 则可以将函数移到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或过时之类的属性, 我们可以修改此范围而不用查找所有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过滤器没有特殊用途, 但是我通常要做的下一步是将功能从Controllers和Model方法移到Model过滤器中。
假设我们还有另一个属性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, 因为这会导致递归保存事件, 并且应用程序的数据库操作层仍然应该是Controller。
步骤9.库
有时候, 你的功能足够大, 可以保证拥有自己的库。你可能希望将其移动到库文件中, 因为它已在很多地方重复使用, 或者它足够大, 因此你希望对其进行单独开发。
可以将库文件存储在lib /目录中, 但是随着文件的增长, 你可以将它们传输到真正的RubyGem中!将代码移入库的主要优点是, 你可以与模型分开测试库。
无论如何, 对于Project List, 我们可以证明将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
注意:当加载Rails Model类并将其作为变量k传入类范围时, 将调用self.included方法。
总结
在本Ruby on Rails重构教程中, 我们花了不到15分钟的时间来实现一个解决方案, 并将其放在进行用户测试的阶段, 准备接受该功能集或将其删除。在重构过程结束时, 我们有一段代码, 为在多个模型中实现可列出, 可激活的项目提供了一个框架, 该框架甚至可以通过最严格的审查过程。
在你自己的Rails重构过程中, 如果你有信心这样做, 可以随意跳过一些步骤(例如, 从View跳到Controller或从Controller跳到Model)。请记住, 代码流是从视图到模型的。
不要害怕看起来很蠢。将现代语言与旧的CGI模板呈现应用程序区分开来的原因并不是我们每次都以正确的方式做一切, 而是花时间重构, 重用和共享我们的努力。
相关:时间戳截断:Ruby on Rails ActiveRecord故事