在 Forge - 使用 metric_fu 检查您的 Ruby 代码

作者:Reuven M. Lerner

在程序员中,长期以来存在着关于语言应该约束他们还是应该给予他们极大灵活性的争论。

如果您已经编程一段时间了,您就会理解双方所宣扬的好处。一种严格的语言可以帮助检查您的代码,通常使用编译器和严格的类型系统,以便在潜在问题进入生产系统之前发现它们。相比之下,更灵活的语言的设计理念是,编译器和严格的类型检查并不能发现所有错误,并且通常会迫使程序员绕过系统的约束,而不是从中受益。

这种简短的描述仅仅是对现代程序员态度的漫画化描绘。但是,它确实指出了程序员在选择语言时经常面临的一种紧张关系。您希望语言在多大程度上约束您?您愿意做出哪些权衡?您是宁愿选择一种不允许您以自己想要的方式表达自己的严格语言,还是选择一种不会阻止您做傻事或危险事的灵活语言?

像许多 Web 开发人员一样,我已经开始偏爱动态、灵活的语言。我不希望语言先发制人地阻止我做事情,即使我正在做的事情可能看起来很疯狂或怪异。在过去的几年里,我变得非常喜欢 Ruby,因为它试图在两者之间取得平衡。

然而,缺少编译器或其他工具来执行常规的健全性检查确实让我有些困扰。我永远不会声称编译器是程序员应该用来测试代码的唯一工具,但它确实执行了第一遍检查,可以提供一些有用的反馈。

幸运的是,Ruby 社区鼓励使用常规的自动化测试来确保代码以您期望的方式工作。如果做得正确,测试实际上可能比编译器和严格的类型检查更好。它可以检查多个级别的代码,反映实际用例,并且不仅可以作为代码语法的健全性检查,还可以作为其逻辑和规范的健全性检查。此外,编写测试迫使程序员反思他们的工作,仔细思考他们是如何实现特定功能的。这种反思是学习过程中必不可少的一部分,它为程序员提供了提高自身技能以及编写更好程序的机会。

因此,自动化测试,以及自动化分析,可以帮助提高程序员的水平,以及他们编写的程序的水平。因此,我很高兴发现了 metric_fu,这是一个来自 Jake Scruggs 和其他人的 Ruby gem,它将一些最著名的分析工具整合到一个方便的 Rails 程序员软件包中。这些各种工具(包括 rcov、Flay 和 Flog)的组合使得查找您编写的代码中的潜在问题并改进它变得容易。自动化分析工具永远不会为您提供 100% 准确的反馈,但获得这种输入总是好的。

本月,我将介绍 metric_fu 以及它为 Rails 程序员提供的一些代码分析工具。诚然,metric_fu “仅仅”是这些单独工具的包装器,但通过使它们如此容易获得并与您的其余测试集成,您将始终能够了解潜在问题可能存在的位置,并在问题给您带来任何实际麻烦之前修复它们。

安装 metric_fu

metric_fu 是一个 Ruby gem,这意味着您可以使用以下命令下载并安装它

sudo gem install metric_fu

metric_fu gem 规范自动要求许多其他它使用的 gem,包括 rcov 和 Flog。因此,安装 metric_fu gem 应该意味着您的系统已准备就绪,无需额外下载和安装。

假设您正在 Rails 中使用 metric_fu,您可能需要告诉 Rails 它应该查找并包含 metric_fu gem。您可以在现代版本的 Rails 中通过将以下行添加到 config/environment.rb 来实现此目的

config.gem 'jscruggs-metric_fu', :version => '0.9.0',
    :lib => 'metric_fu', :source => 'http://gems.github.com'

换句话说,您希望 Rails 加载名为 metric_fu 的 gem,该 gem 可以从 Github 下载,名称为 jscruggs-metric_fu,版本 0.9.0。如果此 gem 不存在,Rails 将退出并显示错误。

最后,您必须在 Rails 应用程序的 Rakefile 中添加一行,告诉它您要加载与 metric_fu 关联的 Rake 任务

require 'metric_fu'

完成此操作后,您应该会发现许多新任务,所有任务的名称都以 metric 开头,可在 Rake 中使用。您可以使用以下命令列出它们

rake -T | grep metrics

我通常运行所有测试,您可以使用以下命令调用这些测试

rake metrics:all

这将运行所有 software metric_fu 可以使用的工具,该列表在过去一年中有所增长。在撰写本文时,运行metrics:all包括

  • churn:哪些文件更改最多?

  • coverage:代码的哪些部分经过测试?

  • flay:代码的哪些部分是重复的?

  • flog:您的代码是否不必要地复杂?

  • reek:您的代码是否受到众所周知的坏习惯的影响?

  • saikuro:您的代码有多复杂?

我在下面更详细地介绍了其中一些测试。但是,在继续之前,重要的是要注意metrics:all如果 rcov 覆盖率工具遇到一个或多个错误,将无法运行所有测试。如果您经常测试,这不是问题,但是如果您破坏了测试然后运行metrics:all.

当您使用以下命令运行完整报告时rake metrics:all,metric_fu 将所有输出文件放在应用程序的 tmp/metric_fu 目录下。每个测试都有自己的单独子目录,并以 HTML 格式生成输出,以便于使用 Web 浏览器阅读。文件放在 tmp/metric_fu 中使得它们易于在本地系统上查找和查看,但是如果您想从远程机器查看它们,则需要将它们移动到 Web 可访问目录(例如,public/tmp/metric_fu)。理所当然的是,您不希望此信息出现在公众可以查看的网站上,因此请务必对这些报告进行密码保护或删除,以避免不愉快的情况。

虽然 metric_fu 的默认设置适用于大多数初始情况,但您可能会发现自己想要自定义其一个或多个测试。您可以在 Rakefile 中通过添加 MetricFu::Configuration 块并调用 config.* 来执行此操作,其中 * 是 metric_fu 引入的测试之一。例如,您可以使用以下命令自定义为 :all 运行的测试

MetricFu::Configuration.run do |config|
  config.metrics          = [:coverage, :flog]
end

如果您修改 config.metrics 以仅包含 metric_fu 测试的子集,您可能会发现当其他测试失败时会感到困惑。例如,如果您将 config.metrics 设置为上述值 [:coverage, :flog],则调用rake metrics:reek将失败,Rake 会抱怨它无法找到这样的任务。

代码覆盖率

metric_fu 家族中最著名的成员可能是 rcov,Ruby 代码覆盖率检查器,由 Mauricio Fernandez 编写。rcov 调用您的所有自动化测试,然后生成一份报告,指示您的源代码文件中哪些行未被这些测试触及。这使您可以精确地看到每个文件的哪些行已经过测试,让您可以专注于以红色突出显示的那些路径(即,未经测试),而不是为已经过测试的代码编写额外的测试。

rcov,由 metric_fu 调用,生成两种基本类型的 HTML 输出。一种提供了网站页面的概述。此输出带有红色和绿色条形图,显示了每个文件已保护的百分比。如果您的任何文件都有部分条形为红色的图表,则会告诉您最初应专注于哪些文件。

但是,一旦您决定确保特定文件具有更好的测试覆盖率,您应该改进哪些行?这就是 rcov 的单个文件输出派上用场的地方。它显示文件的源代码,代码行以绿色(表示它在测试中被覆盖)或红色(表示它未被覆盖)显示。如果您有任何红线,其想法是让您添加测试以强制下次覆盖这些行。当然,如果有不需要存在的红线,rcov 已经帮助您重构了代码,使其更精简。阅读 rcov 的输出非常简单——您希望一切都是绿色的,而不是红色的。任何红色都是邀请您编写更多测试或意识到该代码不再使用并且可以删除。

测试代码的主要原因之一是,当您进行进一步更改时,它会让您感到安心。因此,虽然您可以在没有 100% 测试覆盖率的情况下重构和以其他方式更改代码,但总是可能会遗漏一些东西。因此,rcov 应该是您在使用 metric_fu 时的首要任务。一旦您的代码覆盖率足够高,以确保检测到新问题和更改,您就可以尝试使您的代码更好,而无需更改其功能。

Flog

metric_fu 附带的另一个工具是 Flog,由 Ryan Davis 编写。Flog 生成所谓的“痛苦报告”,识别它认为“受折磨”的代码——痛苦到您真的应该拯救它。即使您不同意它的一些结果,查看 Flog 的输出通常可以为您的代码的复杂性提供一个有趣的视角。它测量变量赋值、代码分支(即,if-then 和 case-when 语句)以及对其他代码的调用,并为每个这些项分配一个分数。总 Flog 分数是 Flog 找到的各个项的总和。

正如 Flog 主页所说,“分数越高,越难测试”。即使您不担心测试,您当然也应该考虑可能在您的项目上工作的其他程序员。复杂的代码难以维护,而维护软件(在我看来)比编写软件更麻烦。因此,通过查看 Flog 的输出,您可以了解您的代码对其他人来说有多难理解。

metric_fu 提供了 Flog 输出的 HTML 版本。我在这里从命令行演示它,可以在命令行中运行为

flog *.rb

这将生成一组简单的输出,例如以下内容,这是我最近在一个小型项目上获得的,并且没有进行太多测试或分析

181.0: flog total
 60.3: flog/method average

 72.5: UploadController#advertiser_file_action
 70.1: UploadController#whitepage_listing_file_action

这似乎表明我的上传控制器有两个不同的方法,这两种方法的复杂程度都相对较高。我可以通过使用 --details 命令行参数调用 Flog 来获得有关这两种方法的更多信息。这给了我以下输出,我已经截断了一些

~/Consulting/Modiinfo/modiinfo/app/controllers$ flog --details
upload_controller.rb
 181.0: flog total
  60.3: flog/method average

  72.5: UploadController#advertiser_file_action
  40.6: assignment
  17.3: branch
   4.8: split
   4.0: blank?
   3.2: strip
   3.2: params
   3.1: +
   3.0: map
   2.8: []
   2.1: downcase

换句话说,Flog 的高分很大一部分是由于 UploadController#advertiser_file_action 中大量的变量赋值。果然,我在该方法中有一堆变量赋值,这导致了高分。例如,我想向最终用户显示上传记录的数量,因此,有以下代码,为实例变量赋值

if advertiser.save
  @number_of_successes = @number_of_successes + 1
else
  @number_of_failures = @number_of_failures + 1
  @error_messages[index] = advertiser.errors
  next
end

我发现这段代码易于阅读和维护,但 Flog 却不这么认为,它更喜欢更函数式的编程风格,方法链接在一起。在这种情况下,我会考虑 Flog 的断言和分数,但我会应用我自己的判断来评估代码的复杂性以及是否需要更改或更新。

Flay

metric_fu 附带的我最喜欢的工具之一是 Flay,也是由 Ryan Davis 编写的,它查找重复代码。良好编码的关键原则之一是 DRY(不要重复自己),Flay 使查找代码可以使用一些额外 DRY-ness 的位置变得容易。通过运行

rake metrics:flay

您将获得一份格式良好的报告,显示代码中存在完全重复的位置(这已经够令人尴尬和成问题的了)和结构重复。因此,如果您在多个控制器中具有相同的变量赋值,Flay 将为您找到这些,并将指出需要重构。例如,我尚未运行 Flay 的简单项目有三种方法,每种方法都包含以下相同的代码

if params[:filename].blank?
  flash[:notice] = 'No file was attached. Please try again.'
  redirect_to :back
  return
end

如果这种代码在同一个控制器中出现三次,则意味着需要进行一些重构。在这种特定情况下,我可以通过将此代码放入单独的方法中,然后通过定义 before_filter 来消除问题

before_filter :check_for_blank_filename,
     :only => [:residence_file_action,
               :advertiser_file_action,
               :whitepage_listing_file_action]

这是该方法,它看起来(毫不奇怪)就像重复的代码

def check_for_blank_filename
 if params[:filename].blank?
   flash[:notice] = 'No file was attached. Please try again.'
   redirect_to :back
   return
 end
end

重新运行 Flay 表明我现在使我的代码比以前更 DRY,提高了其可读性并使其更易于测试。果然,此控制器的 Flay 分数从 392 降至 221。这些度量标准仅相对于彼此有意义,但代码现在更好,并且数字反映了这一点,这似乎是不可否认的。

Flay 还可以找到更微妙的相似之处,指示两段代码看起来彼此相似的位置。例如,我的代码中有以下两行,位于不同的位置

(name, telephone, address, url, email, category_string) =
    line.split("\t").map { |f| f.strip }

(company, telephone, address, url, email, category_string) =
    line.split("\t").map{ |f| f.strip}

Flay 指出,此代码几乎相同,可以重构为更 DRY 一点。我真的会更改此代码吗?可能,也可能不会,但至少我更充分地意识到了这一点,这本身就很重要。如果并且当我花时间重构此代码时,Flay 将指出需要关注的第一个也是最必要的领域。

Reek

最后,我应该提到 Reek,这是一个由 Kevin Rutherford 编写的工具,它也由 metric_fu 调用。Reek 查找“代码异味”或不遵循常用风格的代码。这包括查找代码重复(类似于 Flay 所做的)、长方法和命名不佳的变量。它还尝试查找方法向另一个对象发送的消息多于向自身发送的消息的情况,它称之为功能嫉妒,以及包含超过五行代码的方法,这些方法被标记为过长。

例如,关于我上面提到的代码,该代码读取

(company, telephone, address, url, email, category_string) =
    line.split("\t").map{ |f| f.strip}

Flay 注意到这段代码是重复的。但除此之外,单字母变量名几乎总是一个坏主意,因为它降低了代码的可读性。果然,Reek 会将此代码标记为变量 f 的“不具交流性的名称”。

即使我没有完全被 Rutherford 在 Reek 主页上描述的“Reek 驱动的开发”所折服,Reek 也是一种查找潜在问题并为我正在编写的程序提供额外反馈的有用方法。

结论

由于其动态性和灵活性,Ruby 为程序员提供了做一些可能导致日后维护问题的事情的机会。幸运的是,Ruby 社区已经制作了一套出色的自动化测试和分析工具,使得生成高质量、易于他人理解、测试和维护的代码成为可能。metric_fu 将许多这些工具放入一个软件包中,使得在您的代码上运行各种测试变得容易。

资源

Ruby 语言随附所有现代 Linux 发行版,但可以从 www.ruby-lang.org 下载。Web 开发的 Ruby on Rails 框架位于 www.rubyonrails.com

像许多现代 Ruby gem 一样,metric_fu 托管在 Github 上,Github 是一家商业 git 托管服务,为开源项目提供免费帐户。您可以从 github.com/jscruggs/metric_fu/tree/master 下载 metric_fu。

您可以分别从 github.com/spicycode/rcov/tree/master 下载 rcov,从 github.com/seattlerb/flay/tree/master 下载 Flay,从 github.com/seattlerb/flog/tree/master 下载 Flog,以及从 wiki.github.com/kevinrutherford/reek 下载 Reek。

关于编程语言的本质以及对类型系统和编译器的依赖,有两篇出色的文章,分别是 Steve Yegge 关于动态语言回归的博客文章 (steve-yegge.blogspot.com/2008/05/dynamic-languages-strike-back.html) 和 Bruce Eckel 关于使用测试代替强类型来确保良好代码的文章 (www.mindview.net/WebLog/log-0025)。

Donald Schon 的优秀著作 反思实践者 描述了专业人士在从事工作时可以并且应该反思其工作的不同方式。虽然 Schon 没有专门提到程序员,但他所说的话非常适合编程工作,并使我确信自动化测试和分析工具为何如此有价值。

Reuven M. Lerner 是一位长期的 Web/数据库开发人员和顾问,是西北大学学习科学博士候选人,研究在线学习社区。他在芝加哥地区生活了四年后,最近(与妻子和三个孩子)返回他们在以色列莫迪因的家。

加载 Disqus 评论