简介

Ruby is Big in China Ruby China

关于作者

这篇文章翻译自 bbatsov 所写的 Ruby Style GuideRails Style Guide

关于译者

Juanito Fatas,你可以在 twitter、ruby-china 找到我。

不好意思,我刚学习 Ruby 及 Rails,翻译的质量可能不太好,

有错误不好的地方,麻烦去 github 开任务给我。

提交建议

Ruby Style Guide 建議

Rails Style Guide 建議

为什么翻译此文

几个礼拜前我开始学习 Ruby 及 Rails。我刚好看到了这两个指导,我觉得这可以让我有全观的理解及避免可能犯的错误,这样子再去学习会更有方向,这是我翻译的原因。

而关于风格,我看过一本书,有一段文字,诠释的很好:

Style is necessary only where understanding is missing. A corollary to this is that sometimes the only way to effectively use something you don’t understand is to copy styles observed elsewhere. – From Let Over Lambda

我希望你读的愉快。

内容许可

内容采用创用 CC 2.5 授权释出

Creative Commons License

Ruby 风格指导

序幕

作为 Ruby 开发者,有一件总是令我烦心的事 — Python 开发者有一份好的编程风格参考指南(PEP-8) 而我们永远没有一份官方指南,一份记录 Ruby 编程风格及最佳实践的指南。而我们确信风格很重要。我也相信这些好家伙们,像我们是 Ruby 开发者,应该可以自己产生一份这个梦寐以求的文档。

这份指南开始是作为我们公司内部 Ruby 编程指南(由我所写的)。进行到某个部分时,我决定要把我的成果贡献给广大的 Ruby 社群,而且这个世界需要从另一个公司内部的一点帮助。然而这个世界也可以从由社群制定及策动的一系列 Ruby 编程惯例、实践及风格中受益。

在开始写这份指南时,我收到世界上很多优秀 Ruby 社群用户们的反馈。感谢所有的建议及帮助!我们同心协力创造一个能够让每一个 Ruby 开发者受益的资源。

顺道一提,如果你对 Rails 感兴趣,你可以看看这份互补的 Ruby on Rails 3 风格指南

Ruby 风格指南

这份 Ruby 风格指南向你推荐现实世界中,Ruby 程序员如何写出可被别的Ruby程序员维护的代码。一份风格指南,反映出现实世界中的用法,带有一个理想,帮助人们避免使用危险的代码 — 不管它看起来有对好。

本指南分成数个相关规则的小节。我试着在每个规则后说明理由(如果省略的话,我相信理由是显而易见的)。

我没有想到所有的规则 — 他们大致上是基于,我作为一个专业软体工程师的广泛生涯,从 Ruby 社群成员所得到的反馈及建议,和数个高度评价的 Ruby 程式设计资源,像是 "Programming Ruby 1.9" 以及 "The Ruby Programming Language"

本指南仍在完善中 — 某些规则缺乏实例,某些规则没有例子来清楚地演示它们。在最后交付时,这些议题将会被解决 — 就先把它们记在心理吧。

你可以使用 Transmuter 来产生本指南的一份 PDF 或 HTML 复本。

源代码排版

使用 UTF-8 作为源文件的编码。

每个缩排层级使用两个空格

# 好
def some_method
  do_something
end

# 差 - 四个空格
def some_method
    do_something
end

使用 Unix 风格的行编码(缺省涵盖 BSD/Solaris/Linux/OSX 的用户,Windows 用户要格外小心。) 如果你使用 Git ,你也许会想加入下面这个配置,来保护你的项目被 Windows 的行编码侵入:

$ git config --global core.autocrlf true

使用空格来围绕操作符,逗号 , 、冒号 : 及分号 ; 之后,围绕在 {} 之前。空格可能对(大部分)Ruby 直译器来说是无关紧要的,但正确的使用是写出可读性高的代码的关键。

sum = 1 + 2
a, b = 1, 2
1 > 2 ? true : false; puts 'Hi'
[1, 2, 3].each { |e| puts e }

唯一的例外是当使用指数操作符时:

# 差
e = M * c ** 2

# 好
e = M * c**2

不要有空格在 ([ 之后,或 ]) 之前。

some(arg).other
[1, 2, 3].length

whencase 缩排在同一层。我知道很多人不同意这一点,但这是“The Ruby Programming Language”及“Programming Ruby”所使用的风格。

case
when song.name == 'Misty'
  puts 'Not again!'
when song.duration > 120
  puts 'Too long!'
when Time.now.hour > 21
  puts "It's too late"
else
  song.play
end

kind = case year
       when 1850..1889 then 'Blues'
       when 1890..1909 then 'Ragtime'
       when 1910..1929 then 'New Orleans Jazz'
       when 1930..1939 then 'Swing'
       when 1940..1950 then 'Bebop'
       else 'Jazz'
       end

def 之间使用空行,并且把方法分成合乎逻辑的段落。

def some_method
  data = initialize(options)

  data.manipulate!

  data.result
end

def some_method
  result
end

当一个方法呼叫的参数扩展至多行时,排列它们。

# 一开始(一行太长)
def send_mail(source)
  Mailer.deliver(to: '[email protected]', from: '[email protected]', subject: 'Important message', body: source.text)
end

# 差(一般的缩排)
def send_mail(source)
  Mailer.deliver(
    to: '[email protected]',
    from: '[email protected]',
    subject: 'Important message',
    body: source.text)
end

# 差(两倍缩排)
def send_mail(source)
  Mailer.deliver(
      to: '[email protected]',
      from: '[email protected]',
      subject: 'Important message',
      body: source.text)
end

# 好
def send_mail(source)
  Mailer.deliver(to: '[email protected]',
                 from: '[email protected]',
                 subject: 'Important message',
                 body: source.text)
end

使用 RDoc 以及它的惯例来撰写 API 文档。不要在注解区块及 def 之前放一个空行。 让每一行保持少于 80 个字符。 避免尾随的空白。

语法

使用 def 时,当有参数时使用括号。当方法不接受任何参数时,省略括号。

def some_method
# body omitted
end

def some_method_with_arguments(arg1, arg2)
# body omitted
end

永远不要使用 for ,除非你很清楚为什么。大部分情况应该使用迭代器来取代。 for 是由 each 所实作的(所以你加入了一层的迂回),但出乎意料的是 — for 并没有包含一个新的视野(不像是 each )而在这个区块中定义的变量将会被外部所看到。

arr = [1, 2, 3]

# 差
for elem in arr do
  puts elem
end

# 好
arr.each { |elem| puts elem }

永远不要在多行的 if/unless 使用 then

# 差
if some_condition then
  # body omitted
end

# 好
if some_condition
  # body omitted
end

偏爱三元操作符 ? : 胜于 if/then/else/end 结构。它更为常见及更精准。

# 差
result = if some_condition then something else something_else end

# 好
result = some_condition ? something : something_else

使用一个表达式给一个三元操作符的分支。这也意味着三元操作符不要嵌套。嵌套情况使用 if/else 结构。

# 差
some_condition ? (nested_condition ? nested_something : nested_something_else) : something_else

# 好
if some_condition
  nested_condition ? nested_something : nested_something_else
else
  something_else
end

永远不要使用 if x: ... — 它已经在 Ruby 1.9 被移除了。使用三元操作符来取代。

# 差
result = if some_condition: something else something_else end

# 好
result = some_condition ? something : something_else

永远不要使用 if x: ... 使用三元操作符来取代。

一行的情况使用 when x then ...。替代方案的语法 when x: ... 在 Ruby 1.9 被移除了。

永远不要使用 when x: ...。参考前一个规则。

布尔表达式使用 &&/||,控制流程使用 and/or。 (经验法则:如果你需要使用外部括号,你正在使用错误的操作符。)

# 布尔表达式
if some_condition && some_other_condition
  do_something
end

# 控制流程
document.saved? or document.save!

避免多行的 ? :(三元操作符),使用 if/unless 来取代。

偏爱 if/unless 修饰符当你有单行的主体。

另一个好的方法是使用控制流程的 and/or

# 差
if some_condition
  do_something
end

# 好
do_something if some_condition

# 另一个好方法
some_condition and do_something

否定条件偏爱 unless 优于 if(或是控制流程 or)。

# 差
do_something if !some_condition

# 好
do_something unless some_condition

# 另一个好方法
some_condition or do_something

永远不要使用 unless 搭配 else 。将它们改写成肯定条件。

# 差
unless success?
  puts 'failure'
else
  puts 'success'
end

# 好
if success?
  puts 'success'
else
  puts 'failure'
end

不要使用括号围绕 if/unless/while 的条件式,除非这条件包含了一个赋值(见下面使用 = (一个赋值)的返回值)。

# 差
if (x > 10)
  # body omitted
end

# 好
if x > 10
  # body omitted
end

# 好
if (x = self.next_value)
  # body omitted
end

忽略围绕方法参数的括号,如内部 DSL (如:Rake, Rails, RSpec),Ruby 中带有“关键字”状态的方法(如:attr_reader, puts)以及属性存取方法。

所有其他的方法呼叫使用括号围绕参数。

class Person
  attr_reader :name, :age

# 忽略
end

temperance = Person.new('Temperance', 30)
temperance.name

puts temperance.age

x = Math.sin(y)
array.delete(e)

单行区块喜好 {...} 胜于 do..end。多行区块避免使用 {...}(多行串连总是​​丑陋)。在 do...end 、“控制流程”及“方法定义” ,永远使用 do...end (如 Rakefile 及某些 DSL)。串连时避免使用 do...end

names = ["Bozhidar", "Steve", "Sarah"]

# 好
names.each { |name| puts name }

# 差
names.each do |name|
  puts name
end

# 好
names.select { |name| name.start_with?("S") }.map { |name| name.upcase }

# 差
names.select do |name|
  name.start_with?("S")
end.map { |name| name.upcase }

某些人会争论多行串连时,使用 {...} 看起来还可以,
但他们应该问问自己 — 这样代码真的可读吗
以及不能把区块内容取出来放到绝妙的方法中吗。

避免在不需要的场合时使用 return

# 差
def some_method(some_arr)
  return some_arr.size
end

# 好
def some_method(some_arr)
  some_arr.size
end

当赋予缺省值给方法参数时,使用空格围绕 = 操作符。

# 差
def some_method(arg1=:default, arg2=nil, arg3=[])
  # 做些什么...
end

# 好
def some_method(arg1 = :default, arg2 = nil, arg3 = [])
  # 做些什么...
end


然而几本 Ruby 书建议第一个风格,第二个风格在实践中更为常见
(并可争议地可读性更高一点)。

避免在不需要的场合使用续行 \ 。在实践中,尽量避免使用续行。

# 差
result = 1 - \
         2

# 好 (但仍丑的跟地狱一样)
result = 1 \
         - 2

使用 =(一个赋值)的返回值是好的,但用括号环绕赋值。

# 好 — 演示赋值的目标用途
if (v = array.grep(/foo/)) ...

# 差
if v = array.grep(/foo/) ...

# 也很好 — 演示赋值的目标用途及有正确的优先序
if (v = self.next_value) == "hello" ...

随意使用 ||= 来初始化变量

# 仅在 name 为 nil 或 false 时,把名字设为 Bozhidar。
name ||= 'Bozhidar'

不要使用 ||= 来初始化布尔变量。

(想看看如果现在的值刚好是 false 时会发生什么。)

# 差 — 会把 enabled 设成真,即便它本来是假。
enabled ||= true

# 好
enabled = true if enabled.nil?

避免使用 Perl 风格的特别变量(像是 $0-9, $`, 等等)。它们看起来非常神秘以及不鼓励使用一行的脚本。

避免在方法名与左括号之间放一个空格。

# 差
f (3 + 2) + 1

# 好
f(3 + 2) + 1

如果方法的第一个参数由左括号开始,永远在这个方法呼叫里使用括号。

举个例子,写 f((3+2) + 1)

总是使用 -w 来执行 Ruby 直译器,如果你忘了某个上述的规则,它就会警告你!

当你的哈希键是符号时,使用 Ruby 1.9 哈希字面语法。

# 差
hash = { :one => 1, :two => 2 }

# 好
hash = { one: 1, two: 2 }

使用新的 lambda 字面语法。

# 差
lambda = lambda { |a, b​​| a + b }
lambda.call(1, 2)

# 好
lambda = ->(a, b) { a + b }
lambda.(1, 2)

未使用的区块参数使用 _

# 差
result = hash.map { |k, v| v + 1 }

# 好
result = hash.map { |_, v| v + 1 }

命名

方法与变量使用蛇底式小写( snake_case )。

类别与模组使用驼峰式大小写( CamelCase )。(保留像是HTTP、RFC、XML 这种缩写为大写)

其他常数使用尖叫蛇底式大写( SCREAMING_SNAKE_CASE )。

判断式方法的名字(返回布尔值的方法)应以问号结尾。 (即 Array#empty? )

有潜在“危险性”的方法,若此危险方法有安全版本存在时,应以惊叹号结尾(即:改动 self 或参数、 exit! 等等方法)。

# 不好 - 没有对应的安全方法
class Person
  def update!
  end
end

# 好
class Person
  def update
  end
end

# 好
class Person
  def update!
  end

  def update
  end
end

如果可能的话,从危险方法(bang)的角度来定义对应的安全方法(non-bang)。

class Array
  def flatten_once!
    res = []

    each do |e|
      [*e].each { |f| res << f }
    end

    replace(res)
  end

  def flatten_once
    dup.f​​latten_once!
  end
end

在短的区块使用 reduce 时,把参数命名为 |a, e| (累加器,元素)。

当定义二元操作符时,把参数命名为 other

def +(other)
  # body omitted
end

偏好 map 胜于 collectfind 胜于 detectselect 胜于 find_allreduce 胜于 inject 以及 size 胜于 length 。这不是一个硬性要求;如果使用别名增加了可读性,使用它没关系。

这些有押韵的方法名是从 Smalltalk 继承而来,在别的语言不常见。鼓励使用 select 而不是 find_all 的理由是它跟 reject 搭配起来是一目了然的。

注释 (Comments)

撰写自我记录的代码然后忽略之后的小节。我是认真的!

比一个单词长的注释要大写及使用标点符号。

句号后使用一个空格

避免冗赘的注释

# 差
counter += 1 # 把计数器加一

保持现有的注释是最新的。过时的注解比没有注解还差。

避免替烂代码写注解。重构代码让它们看起来一目了然。

(要嘛就做,要嘛不做― 不要只是试试看。)

注解 (Annotations)

注解应该直接写在相关代码那行之后。

注解关键字后方,伴随着一个冒号及空白,接着一个描述问题的记录。

如果需要用多行来描述问题,之后的行要放在 # 号后面并缩排两个空白。

def bar # FIXME: 这在v3.2.1 版本之后会异常崩溃,或许与 # BarBazUtil 的版本更新有关 baz(:quux) end

在问题是显而易见的情况下,任何的文档会是多余的,注解应该要留在可能有问题的那行。这个用法是例外而不是规则。

def bar
  sleep 100 # OPTIMIZE
end

使用 TODO 来标记之后应被加入的未实现功能或特色。

使用 FIXME 来标记一个需要修复的代码。

使用 OPTIMIZE 来标记可能影响效能的缓慢或效率低落的代码。

使用 HACK 来标记代码异味,其中包含了可疑的编码实践以及应该需要重构。

使用 REVIEW 来标记任何需要审视及确认正常动作的地方。

举例来说:

REVIEW: 我们确定用户现在是这么做的吗?

如果你觉得是适当的话,使用其他习惯的注解关键字,但记得把它们记录在项目的 README 或类似的地方。

类别

当设计类别阶层时,确认它们符合Liskov 代换原则。 尽可能让你的类别越坚固越好。 永远替类别提供一个适当的 to_s 方法给来表示领域模型。

class Person
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end

  def to_s
    "#@first_name #@last_name"
  end
end

使用 attr 这类函数来定义琐碎的 accessor 或 mutators。

# 差
class Person
  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end

  def first_name
    @first_name
  end

  def last_name
    @last_name
  end
end

# 好
class Person
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end
end

考虑使用 Struct.new,它替你定义了那些琐碎的存取器(accessors),建构式(constructor)以及比较操作符(comparison operators)。

# 好
class Person
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end
end

# 较佳
class Person < Struct.new (:first_name, :last_name)
end

考虑加入工厂方法来提供额外合理的方式,创造一个特定类别的实体。

class Person
  def self.create(options_hash)
    # body omitted
  end
end

偏好鸭子类型胜于继承。

# 差
class Animal
  # 抽象方法
  def speak
  end
end

# 继承高层级的类别
class Duck < Animal
  def speak
    puts 'Quack! Quack'
  end
end

# 继承高层级的类别
class Dog < Animal
  def speak
    puts 'Bau! Bau!'
  end
end

# 好
class Duck
  def speak
    puts 'Quack! Quack'
  end
end

class Dog
  def speak
    puts 'Bau! Bau!'
  end
end

由于继承中“讨厌的”行为,避免使用类别变量( @@ )。

class Parent
  @@class_var = 'parent'

  def self.print_class_var
    puts @@class_var
  end
end

class Child < Parent
  @@class_var = 'child'
end

Parent.print_class_var # => will print "child"

如同你所看到的,在类别阶级中的所有类别其实都共享一个类别变量。
应该通常偏好使用实体变量而不是类别变量。

依据方法的目的用途指定适当的可视层级(private , protected )。

别把所有方法都设为 public (方法的缺省值)。

我们现在是在写 Ruby ,不是 Python

public, protected, private 和方法定义有一样的缩排。

在每一个上方留一个空行。

class SomeClass
  def public_method
    # ...
  end

  private
  def private_method
    # ...
  end
end

使用def slef.method 来定义singleton 方法。这让方法更能抵抗重构带来的变化。

class TestClass
  # 差
  def TestClass.some_method
    # body omitted
  end

  # 好
  def self.some_other_method
    # body omitted
  end

  # 也有可能及当你要定义多个
  # singleton时的便利方法
  class << self
    def first_method
      # body omitted
    end

    def second_method_etc
      # body omitted
    end
  end
end

异常

不要封锁异常。

begin
  # 这里发生了一个异常
rescue SomeError
  # 拯救子句完全没有做事
end

不要为了控制流程而使用异常。

# 差
begin
  n / d
rescue ZeroDivisionError
  puts "Cannot divide by 0!"
end

# 好
if d.zero?
  puts "Cannot divide by 0!"
else
  n / d
end

避免救援 Exception 类别。这会把信号困住,并呼叫 exit,导致你需要 kill -9 进程。

# 不好
begin
  # 呼叫 exit 及杀掉信号会被捕捉(除了 kill -9)
  exit
rescue Exception
  puts "you didn't really want to exit, right?"
  # 异常处理
end

# 好
begin
  # 从StandardError 中救援一个救援子句,
  # 不是许多程式设计师所假定的异常。
rescue => e
  # 异常处理
end

# 也很好
begin
  # 这里发生一个异常

rescue StandardError => e
  # 异常处理
end

把较具体的异常放在救援串连的较上层,不然它们永远不会被拯救。

# 差
begin
  # 一些代码
rescue Exception => e
  # 一些处理
rescue StandardError => e
  # 一些处理
end

# 好
begin
  # 一些代码
rescue StandardError => e
  # 一些处理
rescue Exception => e
  # 一些处理
end

在 ensure 区块中释放你的程式的外部资源。

f = File.open("testfile")
begin
  # .. 处理
rescue
  # .. 错误处理
ensure
  f.close unless f.nil?
end

偏爱使用标准函式库的异常处理胜于导入新的异常类别。

集合

当你需要使用一个字串的数组时,偏好使用 %w 的字面数组语法。

# 差
STATES = ['draft', 'open', 'closed']

# 好
STATES = %w(draft open closed)

避免在数组中创造巨大的间隔。

arr = []
arr[100] = 1 # 现在你有一个很多 nil 的数组

当处理独一无二的元素时,使用 Set 来替代 ArraySet 实现了不重复的无序数值集合。 Set 是数组直观的内部操作功能与哈希的快速存取的混合体。

使用符号取代字串作为哈希键。

# 差
hash = { 'one' => 1, 'two' => 2, 'three' => 3 }

# 好
hash = { one: 1, two: 2, three: 3 }

避免使用可变的对象作为键值。

优先使用新的 1.9 字面哈希语法而不是 => (hashrocket) 语法。

# 差
hash = { :one => 1, :two => 2, :three => 3 }

# 好
hash = { one: 1, two: 2, three: 3 }

信任这个事实, 1.9 的哈希是有序的。

在遍历一个集合时,不要改动它。

字串

偏好字串插值(interpolation),而不是字串串接(concatenation)。

# 差
email_with_name = user.name + ' <' + user.email + '>'

# 好
email_with_name = "#{user.name} <#{user.email}>"

当你不需要插入特殊符号如 \t, \n, ', 等等时,偏好单引号的字串。

# 差
name = "Bozhidar"

# 好
name = 'Bozhidar'

不要使用 {} 围绕要被插入字串的实体变量。

class Person
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end

  # 差
  def to_s
    "#{@first_name} #{@last_name}"
  end

  # 好
  def to_s
    "#@first_name #@last_name"
  end
end

当你需要建构庞大的资料区块(chunk)时,避免使用 String#+ 。使用 String#<< 来替代。字串串接在对的地方改变字串实体,并且永远比 String#+ 来得快,String#+ 创造了一堆新的字串对象。

# 好也比较快
html = ''
html << '<h1>Page title</h1>'

paragraphs.each do |paragraph|
  html << "<p>#{paragraph}</p>"
end

正則表示法

如果你只需要在字串中简单的搜索文字,不要使用正則表示法:string['text']

针对简单的字串查询,你可以直接在字串索引中直接使用正則表示法。

match = string[/regexp/] # 获得匹配正則表示法的内容
first_group = string[/text(grp)/, 1] # 或得分组的内容
string[/text (grp)/, 1] = 'replace' # string => 'text replace'

当你不需要替结果分组时,使用非分组的群组。

/(first|second)/ # 差
/(?:first|second)/ # 好

避免使用 $1-9,因为它们很难追踪它们包含什么。可以使用命名群组来替代。

# 差
/(regexp)/ =~ string
...
process $1

# 好
/(?<meaningful_var>regexp)/ =~ string
...
process meaningful_var

字符类别只有几个你需要关心的特殊字元:^, -, \, ],所以你不用逃脱字元 . 或在 [] 的中括号。

小心使用 ^$ ,它们匹配的是一行的开始与结束,​​不是字串的开始与结束。如果你想要匹配整个字串,使用 \A\z

(译注:\Z 实为 /\n?\z/,使用 \z 才能匹配到有含新行的字串的结束)

string = "some injection\nusername"
string[/^username$/] # matches
string[/\Ausername\z/] # don't match

针对复杂的正則表示法,使用 x 修饰符。这让它们的可读性更高并且你可以加入有用的注释。只是要小心忽略的空白。

regexp = %r{
  start # 一些文字
  \s # 空白字元
  (group) # 第一组
  (?:alt1|alt2) # 一些替代方案
  end
}x

针对复杂的替换,subgsub 可以与区块或哈希来使用。

百分比字面

随意使用 %w

STATES = %w(draft open closed)

使用 %() 给需要插值与嵌入双引号的单行字串。多行字串,偏好使用 heredocs 。

# 差(不需要插值)
%(<div class="text">Some text</div>)
# 应该使用'<div class="text">Some text</div>'

# 差(没有双引号)
%(This is #{quality} style)
# 应该使用 "This is #{quality} style"

# 差(多行)
%(<div>\n<span class="big">#{exclamation}</span>\n</div>)
# 应该是一个 heredoc

# 好(需要插值、有双引号以及单行)
%(<tr><td class="name">#{name}</td>)

正則表示法要匹配多于一个的 / 字元时,使用 %r

# 差
%r(\s+)

# 仍然差
%r(^/(.*)$)
# 应当是 /^\/(.*)$/

# 好
%r(^/blog/2011/(.*)$)

避免 %q, %Q, %x, %s 以及 %W。 偏好 () 作为所有 % 字面的分隔符。

元编程

写一个函式库时不要在核心类别捣乱(不要替它们加 monkey patch)

偏好区块形式的 class_eval 胜于字串插值(string-interpolated)的形式。

当你使用字串插值形式时,总是提供 __FILE____LINE__,使你的 backtrace 看起来有意义:

class_eval "def use_relative_model_naming?; true; end", __FILE__, __LINE__

偏好 define_method 胜于 class_eval{ def ... }

当使用 class_eval (或其它的eval)搭配字串插值时,添加一个注解区块,来演示如果做了插值的样子(我从 Rails 代码学来的一个实践):

# 从 activesupport/lib/active_support/core_ext/string/output_safety.rb
UNSAFE_STRING_METHODS.each do |unsafe_method|
  if 'String'.respond_to?(unsafe_method)
    class_eval <<-EOT, __FILE__, __LINE__ + 1
      def #{unsafe_method}(*args, &block)       # def capitalize(*args, &block)
        to_str.#{unsafe_method}(*args, &block)  #   to_str.capitalize(*args, &block)
      end                                       # end

      def #{unsafe_method}!(*args)              # def capitalize!(*args)
        @dirty = true                           #   @dirty = true
        super                                   #   super
      end                                       # end
    EOT
  end
end

元编程避免使用 method_missing。会让 Backtraces 变得很凌乱;行为没有列在 #methods 里;拼错的方法呼叫可能默默的工作(nukes.luanch_state = false)。考虑使用 delegation, proxy, 或是 define_method 来取代。如果你必须使用 method_missing 。确保也定义了respond_to?

仅捕捉字首定义良好的方法,像是 find_by_* ― 让你的代码愈肯定(assertive)愈好。

在最后的叙述句(statement)呼叫 super

从 delegate 到 assertive, 不神奇的(non-magical) methods:

# 差
def method_missing?(meth, *args, &block)
  if /^find_by_(?<prop>.*)/ =~ meth
    # ... lots of code to do a find_by
  else
    super
  end
end

# 好
def method_missing?(meth, *args, &block)
  if /^find_by_(?<prop>.*)/ =~ meth
    find_by(prop, *args, &block)
  else
    super
  end
end

# 而最好是在每个可以找到的属性被宣告时,使用 define_method。

其它

ruby -w 写安全的代码。

避免使用哈希作为选择性参数。这个方法是不是做太多事了?

避免方法长于 10 行代码(LOC)。理想上,大部分的方法会小于 5 行。空行不算进LOC里。

避免参数列表长于三或四个参数。

如果你真的需要,加入“全域”变量到核心以及把它们设为私有的。

使用类别变量而不是全域变量。

# 差
$foo_bar = 1

# 好
class Foo
  class << self
    attr_accessor ​​:bar
  end
end

Foo.bar = 1

alias_method 可以做到时,避免使用 alias

使用 OptionParser 来解析复杂的命令行选项及 ruby -s 来处理琐碎的命令行选项。 用函数式的方法编程,在有意义的情况下避免赋值。

避免不需要的元编程。

不要变动参数,除非那是方法的目的。

避免超过三行的区块嵌套。

保持一致性。

在理想的世界里,遵循这些准则。

使用常识。

貢獻

在本指南所寫的每個東西都不是定案。這只是我渴望想與同樣對 Ruby 程式設計風格有興趣的大家一起工作,以致於最終我們可以替整個 Ruby 社群創造一個有益的資源。

歡迎開票或發送一個帶有改進的更新請求。在此提前感謝你的幫助!

口耳相傳

一份社群策動的風格指南,對一個社群來說,只是讓人知道有這個社群。推特這個指南,分享給你的朋友或同事。我們得到的每個註解、建議或意見都可以讓這份指南變得更好一點。而我們想要擁有的是最好的指南,不是嗎?

Rails 风格指导

序幕

这份指南目的于演示一整套 Rails 3 开发的风格惯例及最佳实践。这是一份与由现存社群所驱动的Ruby 编码风格指南互补的指南。

而本指南中测试 Rails 应用小节摆在开发 Rails 应用之后,因为我相信行为驱动开发 (BDD) 是最佳的软体开发之道。铭记在心吧。

Rails 是一个坚持己见的框架,而这也是一份坚持己见的指南。在我的心里,我坚信 RSpec 优于 Test::Unit,Sass 优于 CSS 以及 Haml,(Slim) 优于 Erb。所以不要期望在这里找到 Test::Unit, CSS 及 Erb 的忠告。

某些忠告仅适用于 Rails 3.1+ 以上版本。

你可以使用 Transmuter 来产生本指南的一份 PDF 或 HTML 复本。

开发 Rails 应用程序

配置

把惯用的初始化代码放在 config/initializers。 在 initializers 内的代码于应用启动时执行。

每一个 gem 相关的初始化代码应当使用同样的名称,放在不同的文件里,如: carrierwave.rb, active_admin.rb, 等等。

相应调整配置开发、测试及生产环境(在 config/environments/ 下对应的文件)

标记额外的资产给(如有任何)预编译:

# config/environments/production.rb
# 预编译额外的资产(application.js, application.css, 以及所有已经被加入的非 JS 或 CSS 的文件)
config.assets.precompile += %w( rails_admin/rails_admin.css rails_admin/rails_admin.js )

创立一个与生产环境(production enviroment)相似的额外 staging 环境。

路由

当你需要加入一个或多个动作至一个 RESTful 资源时(你真的需要吗?),使用 member and collection 路由。

# 差
get 'subscriptions/:id/unsubscribe'
resources :subscriptions

# 好
resources :subscriptions do
  get 'unsubscribe', :on => :member
end

# 差
get 'photos/search'
resources :photos

# 好
resources :photos do
  get 'search', :on => :collection
end

若你需要定义多个 member/collection 路由时,使用替代的区块语法(block syntax)。

resources :subscriptions do
  member do
    get 'unsubscribe'
    # 更多路由
  end
end

resources :photos do
  collection do
    get 'search'
    # 更多路由
  end
end

使用嵌套路由(nested routes)来更佳地表达与 ActiveRecord 模型的关系。

class Post < ActiveRecord::Base
  has_many :comments
end

class Comments < ActiveRecord::Base
  belongs_to :post
end

# routes.rb
resources :posts do
  resources :comments
end

使用命名空间路由来群组相关的行为。

namespace :admin do
  # Directs /admin/products/to Admin::ProductsController
  # (app/controllers/admin/products_controller.rb)
  resources :products
end

不要在控制器里使用留给后人般的疯狂路由(legacy wild controller route)。这种路由会让每个控制器的动作透过 GET 请求存取。

# 非常差
match ':controller(/:action(/:id(.:format)))'

控制器

让你的控制器保持苗条 ― 它们应该只替视图层取出数据且不包含任何业务逻辑(所有业务逻辑应当放在模型里)。

每个控制器的行动应当(理想上)只调用一个除了初始的 findnew 方法。

控制器与视图之间共享不超过两个实例变量(instance variable)。

模型

自由地引入不是 ActiveRecord 的类别吧。

替模型命名有意义(但简短)且不带缩写的名字。

如果你需要支援 ActiveRecord 像是验证行为的模型对象,使用 ActiveAttr gem。

class Message
  include ActiveAttr::Model

  attribute :name
  attribute :email
  attribute :content
  attribute :priority

  attr_accessible :name, :email, :content

  validates_presence_of :name
  validates_format_of :email, :with => /^[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
  validates_length_of :content, :maximum => 500
end

更完整的示例,参考 [RailsCast on the subject](http://railscasts.com/episodes/326-activeattr)。

ActiveRecord

避免改动缺省的 ActiveRecord(表的名字、主键,等等),除非你有一个非常好的理由(像是不受你控制的数据库)。

把宏风格的方法放在类别定义的前面(has_many, validates, 等等)。

偏好 has_many :through 胜于 has_and_belongs_to_many

使用 has_many :through 允许在 join 模型有附加的属性及验证

# 使用 has_and_belongs_to_many
class User < ActiveRecord::Base
  has_and_belongs_to_many :groups
end

class Group < ActiveRecord::Base
  has_and_belongs_to_many :users
end

# 偏好方式 - using has_many :through
class User < ActiveRecord::Base
  has_many :memberships
  has_many :groups, through: :memberships
end

class Membership < ActiveRecord::Base
  belongs_to :user
  belongs_to :group
end

class Group < ActiveRecord::Base
  has_many :memberships
  has_many :users, through: :memberships
end

使用新的 “sexy” validation

当一个惯用的验证使用超过一次或验证是某个正则表达映射时,创建一个惯用的 validator 文件。

# 差
class Person
  validates :email, format: { with: /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i }
end

# 好
class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
  end
end

class Person
  validates :email, email: true
end

所有惯用的验证器应放在一个共享的 gem 。

自由地使用命名的作用域(scope)。

当一个由 lambda 及参数定义的作用域变得过于复杂时,更好的方式是建一个作为同样用途的类别方法,并返回 ActiveRecord::Relation 对象。

注意 update_attribute 方法的行为。

它不运行模型验证(不同于 update_attributes )并且可能把模型状态给搞砸。

使用用户友好的网址。在网址显示具描述性的模型属性,而不只是 id

有不止一种方法可以达成:

覆写模型的 to_param 方法。这是 Rails 用来给对象建构网址的方法。缺省的实作会以字串形式返回该 id 的记录。它可被另一个具人类可读的属性覆写。

class Person
  def to_param
    "#{id} #{name}".parameterize
  end
end

为了要转换成对网址友好 (URL-friendly)的数值,字串应当调用 parameterize。 对象的 id 要放在开头,以便给 ActiveRecord 的 find 方法查找。

使用此 friendly_id gem。它允许藉由某些具描述性的模型属性,而不是用 id 来创建人类可读的网址。

class Person
  extend FriendlyId
  friendly_id :name, use: :slugged
end

查看 gem 文档获得更多关于使用的信息。

ActiveResource

当 HTTP 响应是一个与存在的格式不同的格式时(XML 和 JSON),需要某些额外的格式解析,创一个你惯用的格式,并在类别中使用它。惯用的格式应当实作下列方法:extension, mime_type, encode 以及 decode

module ActiveResource
  module Formats
    module Extend
      module CSVFormat
        extend self

        def extension
          'csv'
        end

        def mime_type
          'text/csv'
        end

        def encode(hash, options = nil)
          # 数据以新格式编码并返回
        end

        def decode(csv)
          # 数据以新格式解码并返回
        end
      end
    end
  end
end

class User < ActiveResource::Base
  self.format = ActiveResource::Formats::Extend::CSVFormat

  ...
end

若 HTTP 请求应当不扩展发送时,覆写 ActiveResource::Baseelement_pathcollection_path 方法,并移除扩展的部份。

class User < ActiveResource::Base
  ...

  def self.collection_path(prefix_options = {}, query_options = nil)
    prefix_options, query_options = split_options(prefix_options) if query_options.nil?
    "#{prefix(prefix_options)}#{collection_name}#{query_string(query_options)}"
  end

  def self.element_path(id, prefix_options = {}, query_options = nil)
    prefix_options, query_options = split_options(prefix_options) if query_options.nil?
    "#{prefix(prefix_options)}#{collection_name}/#{URI.parser.escape id.to_s}#{query_string(query_options)}"
  end
end

如有任何改动网址的需求时,这些方法也可以被覆写。

迁移

schema.rb 保存在版本管控之下。

使用 rake db:scheme:load 取代 rake db:migrate 来初始化空的数据库。

使用 rake db:test:prepare 来更新测试数据库的 schema。

避免在表里设置缺省数据。使用模型层来取代。

def amount
  self[:amount] or 0
end

然而 self[:attr_name] 的使用被视为相当常见的,你也可以考虑使用
更罗嗦的(争议地可读性更高的)read_attribute 来取代:

def amount
  read_attribute(:amount) or 0
end

当编写建设性的迁移时(加入表或栏位),使用 Rails 3.1 的新方式来迁移 - 使用 change 方法取代 updown 方法。

# 过去的方式
class AddNameToPerson < ActiveRecord::Migration
  def up
    add_column :persons, :name, :string
  end

  def down
    remove_column :person, :name
  end
end

# 新的偏好方式
class AddNameToPerson < ActiveRecord::Migration
  def change
    add_column :persons, :name, :string
  end
end

视图

不要直接从视图调用模型层。

不要在视图构造复杂的格式,把它们输出到视图 helper 的一个方法或是模型。

使用 partial 模版与布局来减少重复的代码。

加入 client side validation 至惯用的 validators。 要做的步骤有:

声明一个由 ClientSideValidations::Middleware::Base 而来的自定 validator

    module ClientSideValidations::Middleware
      class Email < Base
        def response
          if request.params[:email] =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
            self.status = 200
          else
            self.status = 404
          end
          super
        end
      end
    end

建立一个新文件 public/javascripts/rails.validations.custom.js.coffee 并在你的 application.js.coffee 文件加入一个它的参照:

    # app/assets/javascripts/application.js.coffee
    #= require rails.validations.custom

添加你的用户端 validator:

    #public/javascripts/rails.validations.custom.js.coffee
    clientSideValidations.validators.remote['email'] = (element, options) ->
      if $.ajax({
        url: '/validators/email.json',
        data: { email: element.val() },
        async: false
      }).status == 404
        return options.message || 'invalid e-mail format'

国际化

视图、模型与控制器里不应使用语言相关设置与字串。这些文字应搬到在 config/locales 下的语言文件里。

当 ActiveRecord 模型的标签需要被翻译时,使用 activerecord 作用域:

en:
  activerecord:
    models:
      user: Member
    attributes:
      user:
        name: "Full name"

然后 User.model_name.human 会返回 “Member” ,
而 User.human_attribute_name("name") 会返回 "Full name"。
这些属性的翻译会被视图作为标签使用。

把在视图使用的文字与 ActiveRecord 的属性翻译分开。 把给模型使用的语言文件放在名为 models 的文件夹,给视图使用的文字放在名为 views 的文件夹。

当使用额外目录的语言文件组织完成时,为了要载入这些目录,要在 application.rb 文件里描述这些目录。

# config/application.rb
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]

把共享的本地化选项,像是日期或货币格式,放在 locales 的根目录下。

使用精简形式的 I18n 方法: I18n.t 来取代 I18n.translate 以及使用 I18n.l 取代 I18n.localize

使用 “惰性” 查询视图中使用的文字。假设我们有以下结构:

en:
  users:
    show:
      title: "User details page"

users.show.title 的数值能这样被 app/views/users/show.html.haml 查询:

= t '.title'

在控制器与模型使用点分隔的键,来取代指定 :scope 选项。点分隔的调用更容易阅读及追踪层级。

# 这样子调用
I18n.t 'activerecord.errors.messages.record_invalid'

# 而不是这样
I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages]

关于 Rails i18n 更详细的信息可以在这里找到 Rails Guides

Assets

利用这个 assets pipeline 来管理应用的结构。

保留 app/assets 给自定的样式表, javascripts, 或图片。

第三方代码如: jQuerybootstrap 应放置在 vendor/assets

当可能的时候,使用 gem 化的 assets 版本。(如: jquery-rails)。

Mailers

把 mails 命名为 SomethingMailer。 没有 Mailer 字根的话,不能立即显现哪个是一个 Mailer,以及哪个视图与它有关。

提供 HTML 与纯文本视图模版。

在你的开发环境启用信件失败发送错误。这些错误缺省是被停用的。

# config/environments/development.rb

config.action_mailer.raise_delivery_errors = true

在开发模式使用 smtp.gmail.com 设置 SMTP 服务器(当然了,除非你自己有本地 SMTP 服务器)。

# config/environments/development.rb

config.action_mailer.smtp_settings = {
  address: 'smtp.gmail.com',
  # 更多设置
}

提供缺省的配置给主机名。

# config/environments/development.rb
config.action_mailer.default_url_options = {host: "#{local_ip}:3000"}


# config/environments/production.rb
config.action_mailer.default_url_options = {host: 'your_site.com'}

# 在你的 mailer 类
default_url_options[:host] = 'your_site.com'

如果你需要在你的网站使用一个 email 链结,总是使用 _url 方法,而不是 _path 方法。 _url 方法包含了主机名,而 _path 方法没有。

# 错误
You can always find more info about this course
= link_to 'here', url_for(course_path(@course))

# 正确
You can always find more info about this course
= link_to 'here', url_for(course_url(@course))

正确地显示寄与收件人地址的格式。使用下列格式:

# 在你的 mailer 类别
default from: 'Your Name <[email protected]_site.com>'

确定测试环境的 email 发送方法设置为 test

# config/environments/test.rb

config.action_mailer.delivery_method = :test

开发与生产环境的发送方法应为 smtp

# config/environments/development.rb, config/environments/production.rb

config.action_mailer.delivery_method = :smtp

当发送 HTML email 时,所有样式应为行内样式,由于某些用户有关于外部样式的问题。某种程度上这使得更难管理及造成代码重用。有两个相似的 gem 可以转换样式,以及将它们放在对应的 html 标签里: premailer-rails3roadie

应避免页面产生响应时寄送 email。若多个 email 寄送时,造成了页面载入延迟,以及请求可能逾时。使用 delayed_job gem 的帮助来克服在背景处理寄送 email 的问题。

Bundler

把只给开发环境或测试环境的 gem 适当地分组放在 Gemfile 文件中。

在你的项目中只使用公认的 gem。 如果你考虑引入某些显为人所知的 gem ,你应该先仔细复查一下它的源代码。

关于多个开发者使用不同操作系统的项目,操作系统相关的 gem 缺省会产生一个经常变动的 Gemfile.lock 。 在 Gemfile 文件里,所有与 OS X 相关的 gem 放在 darwin 群组,而所有 Linux 相关的 gem 放在 linux 群组:

# Gemfile
group :darwin do
  gem 'rb-fsevent'
  gem 'growl'
end

group :linux do
  gem 'rb-inotify'
end

要在对的环境获得合适的 gem,添加以下代码至 config/application.rb :

platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym
Bundler.require(platform)

不要把 Gemfile.lock 文件从版本控制里移除。这不是随机产生的文件 - 它确保你所有的组员执行 bundle install 时,获得相同版本的 gem 。

无价的 Gems

一个最重要的编程理念是 "不要重造轮子!" 。若你遇到一个特定问题,你应该要在你开始前,看一下是否有存在的解决方案。下面是一些在很多 Rails 项目中 "无价的" gem 列表(全部兼容 Rails 3.1):

active_admin - 有了 ActiveAdmin,创建 Rails 应用的管理介面就像儿戏。你会有一个很好的仪表盘,图形化 CRUD 介面以及更多东西。非常灵活且可客制化。

capybara - Capybara 旨在简化整合测试 Rack 应用的过程,像是 Rails、Sinatra 或 Merb。Capybara 模拟了真实用户使用 web 应用的互动。 它与你测试在运行的驱动无关,并原生搭载 Rack::Test 及 Selenium 支持。透过外部 gem 支持 HtmlUnit、WebKit 及 env.js 。与 RSpec & Cucumber 一起使用时工作良好。

carrierwave - Rails 最后一个文件上传解决方案。支持上传档案(及很多其它的酷玩意儿的)的本地储存与云储存。图片后处理与 ImageMagick 整合得非常好。

clientsidevalidations - 一个美妙的 gem,替你从现有的服务器端模型验证自动产生 Javascript 用户端验证。高度推荐!

compass-rails - 一个优秀的 gem,添加了某些 css 框架的支持。包括了 sass mixin 的蒐集,让你减少 css 文件的代码并帮你解决浏览器兼容问题。

cucumber-rails - Cucumber 是一个由 Ruby 所写,开发功能测试的顶级工具。 cucumber-rails 提供了 Cucumber 的 Rails 整合。

devise - Devise 是 Rails 应用的一个完整解决方案。多数情况偏好使用 devise 来开始你的客制验证方案。

fabrication - 一个很好的假数据产生器(编辑者的选择)。

factory_girl - 另一个 Fabrication 的选择。一个成熟的假数据产生器。 Fabrication 的精神领袖先驱。

faker - 实用的 gem 来产生仿造的数据(名字、地址,等等)。

feedzirra - 非常快速及灵活的 RSS 或 Atom 种子解析器。

friendly_id - 透过使用某些具描述性的模型属性,而不是使用 id,允许你创建人类可读的网址。

guard - 极佳的 gem 监控文件变化及任务的调用。搭载了很多实用的扩充。远优于 autotest 与 watchr。

haml-rails - haml-rails 提供了 Haml 的 Rails 整合。

haml - Haml 是一个简洁的模型语言,被很多人认为(包括我)远优于 Erb。

kaminari - 很棒的分页解决方案。

machinist - 假数据不好玩,Machinist 才好玩。

rspec-rails - RSpec 是 Test::MiniTest 的取代者。我不高度推荐 RSpec。 rspec-rails 提供了 RSpec 的 Rails 整合。

simpleform - 一旦用过 simpleform(或 formatastic),你就不想听到关于 Rails 缺省的表单。它是一个创造表单很棒的DSL。

simplecov-rcov - 为了 SimpleCov 打造的 RCov formatter。若你想使用 SimpleCov 搭配 Hudson 持续整合服务器,很有用。

simplecov - 代码覆盖率工具。不像 RCov,完全兼容 Ruby 1.9。产生精美的报告。必须用!

slim - Slim 是一个简洁的模版语言,被视为是远远优于 HAML(Erb 就更不用说了)的语言。唯一会阻止我大规模地使用它的是,主流 IDE 及编辑器对它的支持不好。但它的效能是非凡的。

spork - 一个给测试框架(RSpec 或 现今 Cucumber)用的 DRb 服务器,每次运行前确保分支出一个乾净的测试状态。 简单的说,预载很多测试环境的结果是大幅降低你的测试启动时间,绝对必须用!

sunspot - 基于 SOLR 的全文检索引擎。

这不是完整的清单,以及其它的 gem 也可以在之后加进来。以上清单上的所有 gems 皆经测试,处于活跃开发阶段,有社群以及代码的质量很高。

缺陷的 Gems

这是一个有问题的或被别的 gem 取代的 gem 清单。你应该在你的项目里避免使用它们。

rmagick - 这个 gem 因大量消耗内存而声名狼藉。使用 minimagick 来取代。

autotest - 自动测试的老旧解决方案。远不如 guard 及 watchr

rcov - 代码覆盖率工具,不兼容 Ruby 1.9。使用 SimpleCov 来取代。

therubyracer - 极度不鼓励在生产模式使用这个 gem,它消耗大量的内存。我会推荐使用 Mustang 来取代。

这仍是一个完善中的清单。请告诉我受人欢迎但有缺陷的 gems 。

管理进程

若你的项目依赖各种外部的进程使用 foreman 来管理它们。

测试 Rails 应用

也许 BDD 方法是实作一个新功能最好的方法。你从开始写一些高阶的测试(通常使用 Cucumber),然后使用这些测试来驱使你实作功能。一开始你给功能的视图写测试,并使用这些测试来创建相关的视图。之后,你创建丢给视图数据的控制器测试来实现控制器。最后你实作模型的测试以及模型自身。

Cucumber

@wip (工作进行中)标签标记你未完成的场景。这些场景不纳入考虑,且不标记为测试失败。当完成一个未完成场景且功能测试通过时,为了把此场景加至测试套件里,应该移除 @wip 标签。

配置你的缺省配置文件,排除掉标记为 @javascript 的场景。它们使用浏览器来测试,推荐停用它们来增加一般场景的执行速度。 替标记著 @javascript 的场景配置另一个配置文件。

配置文件可在 cucumber.yml 文件里配置。

# 配置文件的定义:
profile_name: --tags @tag_name

带指令运行一个配置文件:

cucumber -p profile_name

若使用 fabrication 来替换假数据 (fixtures),使用预定义的 fabrication steps

不要使用旧版的 web_steps.rb 步骤定义!最新版 Cucumber 已移除 web steps,使用它们导致冗赘的场景,而且它并没有正确地反映出应用的领域。

当检查一元素的可视文字时,检查元素的文字而不是检查 id。这样可以查出 i18n 的问题。

给同种类对象创建不同的功能特色:

# 差
Feature: Articles
# ... 功能实作 ...

# 好
Feature: Article Editing
# ... 功能实作 ...

Feature: Article Publishing
# ... 功能实作 ...

Feature: Article Search
# ... 功能实作 ...

每一个功能有三个主要成分:

Title 标题

Narrative - 简短说明这个特色关于什么。

Acceptance criteria - 每个由独立步骤组成的一套场景。

最常见的格式称为 Connextra 格式。

In order to [benefit] ...
A [stakeholder]...
Wants to [feature] ...

这是最常见但不是要求的格式,叙述可以是依赖功能复杂度的任何文字。

自由地使用场景概述使你的场景备作它用 (keep your scenarios DRY)。

Scenario Outline: User cannot register with invalid e-mail
  When I try to register with an email "<email>"
  Then I should see the error message "<error>"

Examples:
  |email         |error                 |
  |              |The e-mail is required|
  |invalid email |is not a valid e-mail |

场景的步骤放在 step_definitions 目录下的 .rb 文件。

步骤文件命名惯例为 [description]_steps.rb

步骤根据不同的标准放在不同的文件里。

每一个功能可能有一个步骤文件 ( home_page_steps.rb )。

也可能给每个特定对象的功能,建一个步骤文件 (articles_steps.rb)。

使用多行步骤参数来避免重复

场景: User profile
  Given I am logged in as a user "John Doe" with an e-mail "[email protected]"
  When I go to my profile
  Then I should see the following information:
    |First name|John         |
    |Last name |Doe          |
    |E-mail    |[email protected]|

# 步骤:
Then /^I should see the following information:$/ do |table|
  table.raw.each do |field, value|
    find_field(field).value.should =~ /#{value}/
  end
end

使用复合步骤使场景备作它用 (Keep your scenarios DRY)

# ...
When I subscribe for news from the category "Technical News"
# ...

# 步骤:
When /^I subscribe for news from the category "([^"]*)"$/ do |category|
  steps %Q{
    When I go to the news categories page
    And I select the category #{category}
    And I click the button "Subscribe for this category"
    And I confirm the subscription
  }
end

总是使用 Capybara 否定匹配来取代正面情况搭配 should_not,它们会在给定的超时时重试匹配,允许你测试 ajax 动作。见 Capybara 的 读我文件获得更多说明。

RSpec

一个例子仅用一个期望值。

# 差
describe ArticlesController do
  #...

  describe 'GET new' do
    it 'assigns new article and renders the new article template' do
      get :new
      assigns[:article].should be_a_new Article
      response.should render_template :new
    end
  end

  # ...
end

# 好
describe ArticlesController do
  #...

  describe 'GET new' do
    it 'assigns a new article' do
      get :new
      assigns[:article].should be_a_new Article
    end

    it 'renders the new article template' do
      get :new
      response.should render_template :new
    end
  end

end

大量使用 descibecontext

如下地替 describe 区块命名:

非方法使用 “description”

实例方法使用井字号 “#method”

类别方法使用点 “.method”

class Article
  def summary
    #...
  end

  def self.latest
    #...
  end
end

# the spec...
describe Article
  describe '#summary'
    #...
  end

  describe '.latest'
    #...
  end
end

使用 fabricators 来创建测试对象。

大量使用 mocks 与 stubs。

# mocking 一个模型
article = mock_model(Article)

# stubbing 一个方法
Article.stub(:find).with(article.id).and_return(article)

当 mocking 一个模型时,使用 as_null_object 方法。它告诉输出仅监听我们预期的讯息,并忽略其它的讯息。

article = mock_model(Article).as_null_object

使用 let 区块而不是 before(:all) 区块替 spec 例子创建数据。let 区块会被惰性求值。

# 使用这个:
let(:article) { Fabricate(:article) }

# ... 而不是这个:
before(:each) { @article = Fabricate(:article) }

当可能时,使用 subject

describe Article do
  subject { Fabricate(:article) }

  it 'is not published on creation' do
    subject.should_not be_published
  end
end

如果可能的话,使用 specify。它是 it 的同义词,但在没 docstring 的情况下可读性更高。

# 差
describe Article do
  before { @article = Fabricate(:article) }

  it 'is not published on creation' do
    @article.should_not be_published
  end
end

# 好
describe Article do
  let(:article) { Fabricate(:article) }
  specify { article.should_not be_published }
end

当可能时,使用 its

# 差
describe Article do
  subject { Fabricate(:article) }

  it 'has the current date as creation date' do
    subject.creation_date.should == Date.today
  end
end

# 好
describe Article do
  subject { Fabricate(:article) }
  its(:creation_date) { should == Date.today }
end

视图

视图测试的目录结构要与 app/views 之中的相符。

举例来说,在 app/views/users 视图被放在 spec/views/users

视图测试的命名惯例是添加 _spec.rb 至视图名字之后,举例来说,视图 _form.html.haml 有一个对应的测试叫做 _form.html.haml_spec.rb

每个视图测试文件都需要 spec_helper.rb

外部描述区块使用不含 app/views 部分的视图路径。 render 方法没有传入参数时,是这么使用的。

# spec/views/articles/new.html.haml_spec.rb
require 'spec_helper'

describe 'articles/new.html.haml' do
  # ...
end

永远在视图测试来 mock 模型。视图的目的只是显示信息。

assign 方法提供由控制器提供视图使用的实例变量(instance variable)。

# spec/views/articles/edit.html.haml_spec.rb
describe 'articles/edit.html.haml' do
it 'renders the form for a new article creation' do
  assign(
    :article,
    mock_model(Article).as_new_record.as_null_object
  )
  render
  rendered.should have_selector('form',
    method: 'post',
    action: articles_path
  ) do |form|
    form.should have_selector('input', type: 'submit')
  end
end

偏好 capybara 否定情况选择器,胜于搭配正面情况的 should_not 。

# 差
page.should_not have_selector('input', type: 'submit')
page.should_not have_xpath('tr')

# 好
page.should have_no_selector('input', type: 'submit')
page.should have_no_xpath('tr')

当一个视图使用 helper 方法时,这些方法需要被 stubbed。Stubbing 这些 helper 方法是在 template 完成的:

# app/helpers/articles_helper.rb
class ArticlesHelper
  def formatted_date(date)
    # ...
  end
end

# app/views/articles/show.html.haml
= "Published at: #{formatted_date(@article.published_at)}"

# spec/views/articles/show.html.haml_spec.rb
describe 'articles/show.html.html' do
  it 'displays the formatted date of article publishing'
    article = mock_model(Article, published_at: Date.new(2012, 01, 01))
    assign(:article, article)

    template.stub(:formatted_date).with(article.published_at).and_return '01.01.2012'

    render
    rendered.should have_content('Published at: 01.01.2012')
  end
end

spec/helpers 目录的 helper specs 是与视图 specs 分开的。

控制器

Mock 模型及 stub 他们的方法。测试控制器时不应依赖建模。

仅测试控制器需负责的行为:

执行特定的方法

从动作返回的数据 - assigns, 等等。

从动作返回的结果 - template render, redirect, 等等。

# 常用的控制器 spec 示例
# spec/controllers/articles_controller_spec.rb
# 我们只对控制器应执行的动作感兴趣
# 所以我们 mock 模型及 stub 它的方法
# 并且专注在控制器该做的事情上

describe ArticlesController do
  # 模型将会在测试中被所有控制器的方法所使用
  let(:article) { mock_model(Article) }

  describe 'POST create' do
    before { Article.stub(:new).and_return(article) }

    it 'creates a new article with the given attributes' do
      Article.should_receive(:new).with(title: 'The New Article Title').and_return(article)
      post :create, message: { title: 'The New Article Title' }
    end

    it 'saves the article' do
      article.should_receive(:save)
      post :create
    end

    it 'redirects to the Articles index' do
      article.stub(:save)
      post :create
      response.should redirect_to(action: 'index')
    end
  end
end

当控制器根据不同参数有不同行为时,使用 context。

# 一个在控制器中使用 context 的典型例子是,对象正确保存时,使用创建,保存失败时更新。

describe ArticlesController do
  let(:article) { mock_model(Article) }

  describe 'POST create' do
    before { Article.stub(:new).and_return(article) }

    it 'creates a new article with the given attributes' do
      Article.should_receive(:new).with(title: 'The New Article Title').and_return(article)
      post :create, article: { title: 'The New Article Title' }
    end

    it 'saves the article' do
      article.should_receive(:save)
      post :create
    end

    context 'when the article saves successfully' do
      before { article.stub(:save).and_return(true) }

      it 'sets a flash[:notice] message' do
        post :create
        flash[:notice].should eq('The article was saved successfully.')
      end

      it 'redirects to the Articles index' do
        post :create
        response.should redirect_to(action: 'index')
      end
    end

    context 'when the article fails to save' do
      before { article.stub(:save).and_return(false) }

      it 'assigns @article' do
        post :create
        assigns[:article].should be_eql(article)
      end

      it 're-renders the "new" template' do
        post :create
        response.should render_template('new')
      end
    end
  end
end

模型

不要在自己的测试里 mock 模型。

使用捏造的东西来创建真的对象。

Mock 别的模型或子对象是可接受的。

在测试里建立所有例子的模型来避免重复。

describe Article
  let(:article) { Fabricate(:article) }
end

加入一个例子确保捏造的模型是可行的。

describe Article
  it 'is valid with valid attributes' do
    article.should be_valid
  end
end

当测试验证时,使用 have(x).errors_on 来指定要被验证的属性。使用 be_valid 不保证问题在目的的属性。

# 差
describe '#title'
  it 'is required' do
    article.title = nil
    article.should_not be_valid
  end
end

# 偏好
describe '#title'
  it 'is required' do
    article.title = nil
    article.should have(1).error_on(:title)
  end
end

替每个有验证的属性加另一个 describe

describe Article
  describe '#title'
    it 'is required' do
      article.title = nil
      article.should have(1).error_on(:title)
    end
  end
end

当测试模型属性的独立性时,把其它对象命名为 another_object

describe Article
  describe '#title'
    it 'is unique' do
      another_article = Fabricate.build(:article, title: article.title)
      article.should have(1).error_on(:title)
    end
  end
end

Mailers

在 Mailer 测试的模型应该要被 mock。 Mailer 不应依赖建模。

Mailer 的测试应该确认如下:

这个 subject 是正确的

这个 receiver e-mail 是正确的

这个 e-mail 寄送至对的邮件地址

这个 e-mail 包含了需要的信息

describe SubscriberMailer
  let(:subscriber) { mock_model(Subscription, email: '[email protected]', name: 'John Doe') }

  describe 'successful registration email'
    subject { SubscriptionMailer.successful_registration_email(subscriber) }

    its(:subject) { should == 'Successful Registration!' }
    its(:from) { should == ['[email protected]_site.com'] }
    its(:to) { should == [subscriber.email] }

    it 'contains the subscriber name' do
      subject.body.encoded.should match(subscriber.name)
    end
  end
end

Uploaders

我们如何测试上传器是否正确地调整大小。这里是一个 carrierwave 图片上传器的示例 spec:

# rspec/uploaders/person_avatar_uploader_spec.rb
require 'spec_helper'
require 'carrierwave/test/matchers'

describe PersonAvatarUploader do
  include CarrierWave::Test::Matchers

  # 在执行例子前启用图片处理
  before(:all) do
    UserAvatarUploader.enable_processing = true
  end

  # 创建一个新的 uploader。模型被模仿为不依赖建模时的上传及调整图片。
  before(:each) do
    @uploader = PersonAvatarUploader.new(mock_model(Person).as_null_object)
    @uploader.store!(File.open(path_to_file))
  end

  # 执行完例子时停用图片处理
  after(:all) do
    UserAvatarUploader.enable_processing = false
  end

  # 测试图片是否不比给定的维度长
  context 'the default version' do
    it 'scales down an image to be no larger than 256 by 256 pixels' do
      @uploader.should be_no_larger_than(256, 256)
    end
  end

  # 测试图片是否有确切的维度
  context 'the thumb version' do
    it 'scales down an image to be exactly 64 by 64 pixels' do
      @uploader.thumb.should have_dimensions(64, 64)
    end
  end
end

延伸阅读

有几个绝妙讲述 Rails 风格的资源,若有闲暇时应当考虑延伸阅读:

贡献

在本指南所写的每个东西都不是定案。这只是我渴望想与同样对 Rails 编码风格有兴趣的大家一起工作,以致于最终我们可以替整个 Ruby 社群创造一个有益的资源。

欢迎开票或发送一个带有改进的更新请求。在此提前感谢你的帮助!

口耳相传

一份社群驱动的风格指南,对一个社群来说,只是让人知道有这个社群。微博转发这份指南,分享给你的朋友或同事。我们得到的每个注解、建议或意见都可以让这份指南变得更好一点。而我们想要拥有的是最好的指南,不是吗?