编程规范-ruby中我不熟识的

今天review了一次ruby编程规范,把一些之前我不熟识的在这里记录下来。

  • 对于没有主体的类,倾向使用单行定义。 [link]
# 差
class FooError < StandardError
end

# 勉强可以
class FooError < StandardError; end

# 好
FooError = Class.new(StandardError)
  • 把 when与 case缩排在同一层级。这是《Programming Ruby》与《The Ruby Programming Language》中早已确立的风格。 [link]
# 差
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

# 好
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
  • 当将一个条件表达式的结果赋值给一个变量时,保持分支缩排在同一层级。 [link]
# 差 - 非常费解
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

result = if some_cond
  calc_something
else
  calc_something_else
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

result = if some_cond
           calc_something
         else
           calc_something_else
         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

result =
  if some_cond
    calc_something
  else
    calc_something_else
  end
  • 使用 _ 语法改善大数的数值字面量的可读性。 [link]
# 差 - 有几个零?
num = 1000000

# 好 - 方便人脑理解
num = 1_000_000
  • 使用 &&= 预先检查变量是否存在,如果存在,则做相应动作。使用 &&= 语法可以省去 if检查。 [link]
# 差
if something
  something = something.downcase
end

# 差
something = something ? something.downcase : nil

# 勉强可以
something = something.downcase if something

# 好
something = something && something.downcase

# 更好
something &&= something.downcase
  • 未被使用的区块参数或局部变量,添加 _ 前缀或直接使用 _(尽管表意性略差)。这种做法可以抑制 Ruby 解释器或 RuboCop 等工具发出“变量尚未使用”的警告。 [link]
# 差
result = hash.map { |k, v| v + 1 }

def something(x)
  unused_var, used_var = something_else(x)
  # ...
end

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

def something(x)
  _unused_var, used_var = something_else(x)
  # ...
end

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

def something(x)
  _, used_var = something_else(x)
  # ...
end
  • 使用 $stdout/$stderr/$stdin而不是 STDOUT/STDERR/STDIN。STDOUT/STDERR/STDIN是常量,尽管在 Ruby 中允许给常量重新赋值(可能是重定向某些流),但解释器会发出警告。 [link]

  • 倾向使用 sprintf或其别名 format而不是相当晦涩的 String#% 方法。 [link]

# 差
'%d %d' % [20, 10]
# => '20 10'

# 好
sprintf('%d %d', 20, 10)
# => '20 10'

# 好
sprintf('%{first} %{second}', first: 20, second: 10)
# => '20 10'

format('%d %d', 20, 10)
# => '20 10'

# 好
format('%{first} %{second}', first: 20, second: 10)
# => '20 10'
  • 当你希望处理的变量类型是数组,但不太确定其是否真的是数组时,通过使用 [*var] 或 Array()来替代显式的数组类型检查与转换。 [link]
# 差
paths = [paths] unless paths.is_a? Array
paths.each { |path| do_something(path) }

# 好
[*paths].each { |path| do_something(path) }

# 好 - 并且更具可读性
Array(paths).each { |path| do_something(path) }
  • 通过使用范围或 Comparable#between?来替代复杂的比较逻辑。 [link]
# 差
do_something if x >= 1000 && x <= 2000

# 好
do_something if (1000..2000).include?(x)

# 好
do_something if x.between?(1000, 2000)
  • 倾向使用 flat_map而不是 map + flatten的组合。此规则并不适用于深度超过 2 层的数组。举例来说,如果 users.first.songs == ['a', ['b','c']]成立,则使用 map + flatten的组合而不是 flat_map。flat_map只能平坦化一个层级,而 flatten能够平坦化任意多个层级。 [link]
# 差
all_songs = users.map(&:songs).flatten.uniq

# 好
all_songs = users.flat_map(&:songs).uniq
  • 倾向使用 reverse_each 而不是 reverse.each,因为某些混入 Enumerable 模块的类可能会提供 reverse_each 的高效版本。即使这些类没有提供专门特化的版本,继承自 Enumerable 的通用版本至少能保证性能与 reverse.each 相当。 [link]
# 差
array.reverse.each { ... }

# 好
array.reverse_each { ... }

关于注释的一些说明

  • 使用 TODO 标记应当加入的特征与功能。 [link]

  • 使用 FIXME 标记需要修复的代码。 [link]

  • 使用 OPTIMISE 标记可能引发性能问题的低效代码。 [link]

  • 使用 HACK 标记代码异味,即那些应当被重构的可疑编码习惯。 [link]

  • 使用 REVIEW 标记需要确认与编码意图是否一致的可疑代码。比如,REVIEW: Are we sure this is how the client does X currently?。 [link]

类定义

  • 在类定义中,使用一致的结构。 [link]
class Person
  # 首先是 extend 与 include
  extend SomeModule
  include AnotherModule

  # 内部类
  CustomErrorKlass = Class.new(StandardError)

  # 接着是常量
  SOME_CONSTANT = 20

  # 接下来是属性宏
  attr_reader :name

  # 跟着是其他宏(如果有的话)
  validates :name

  # 公开的类方法接在下一行
  def self.some_method
  end

  # 初始化方法在类方法和实例方法之间
  def initialize
  end

  # 跟着是公开的实例方法
  def some_method
  end

  # 受保护及私有的方法等放在接近结尾的地方
  protected

  def some_protected_method
  end

  private

  def some_private_method
  end
end
  • 如果嵌套类数目较多,进而导致外围类定义较长,则将它们从外围类中提取出来,分别放置在单独的以嵌套类命名的文件中,并将文件归类至以外围类命名的文件夹下。 [link]
# 差

# foo.rb
class Foo
  class Bar
    # 定义 30 多个方法
  end

  class Car
    # 定义 20 多个方法
  end

  # 定义 30 多个方法
end

# 好

# foo.rb
class Foo
  # 定义 30 多个方法
end

# foo/bar.rb
class Foo
  class Bar
    # 定义 30 多个方法
  end
end

# foo/car.rb
class Foo
  class Car
    # 定义 20 多个方法
  end
end
  • 定义只有类方法的数据类型时,倾向使用模块而不是类。只有当需要实例化时才使用类。 [link]
# 差
class SomeClass
  def self.some_method
    # 省略主体
  end

  def self.some_other_method
    # 省略主体
  end
end

# 好
module SomeModule
  module_function

  def some_method
    # 省略主体
  end

  def some_other_method
    # 省略主体
  end
end
  • 当你想将模块的实例方法变成类方法时,倾向使用 module_function而不是 extend self。 [link]
# 差
module Utilities
  extend self

  def parse_something(string)
    # 做一些事情
  end

  def other_utility_method(number, string)
    # 做一些事情
  end
end

# 好
module Utilities
  module_function

  def parse_something(string)
    # 做一些事情
  end

  def other_utility_method(number, string)
    # 做一些事情
  end
end
  • 优先考虑通过工厂方法的方式创建某些具有特定意义的实例对象。 [link]
class Person
  def self.create(options_hash)
    # 省略主体
  end
end
  • 尽可能隐式地使用 begin/rescue/ensure/end区块。 [link]
# 差
def foo
  begin
    # 主逻辑
  rescue
    # 异常处理逻辑
  end
end

# 好
def foo
  # 主逻辑
rescue
  # 异常处理逻辑
end
  • 当创建一组元素为单词(没有空格或特殊字符)的数组时,倾向使用 %w而不是 []。此规则只适用于数组元素有两个或以上的时候。 [link]
# 差
STATES = ['draft', 'open', 'closed']

# 好
STATES = %w(draft open closed)
  • 当创建一组符号类型的数组(且不需要保持 Ruby 1.9 兼容性)时,倾向使用 %i 。此规则只适用于数组元素有两个或以上的时候。 [link]
# 差
STATES = [:draft, :open, :closed]

# 好
STATES = %i(draft open closed)
  • 当访问集合中的元素时,倾向使用对象所提供的方法进行访问,而不是直接调用对象属性上的 [n]方法。这种做法可以防止你在 nil 对象上调用 []。 [link]
# 差
Regexp.last_match[1]

# 好
Regexp.last_match(1)
  • 当你需要构造巨大的数据块时,避免使用 String#+,使用 String#<<来替代。String#<<通过修改原对象进行拼接工作,其比 String#+ 效率更高,因为后者需要产生一堆新的字符串对象。 [link]
# 差
html = ''
html += '<h1>Page title</h1>'

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

# 好 - 并且效率更高
html = ''
html << '<h1>Page title</h1>'

paragraphs.each do |paragraph|
  html << "<p>#{paragraph}</p>"
end
  • 当存在更快速、更专业的替代方案时,不要使用 String#gsub。 [link]
url = 'http://example.com'
str = 'lisp-case-rules'

# 差
url.gsub('http://', 'https://')
str.gsub('-', '_')

# 好
url.sub('http://', 'https://')
str.tr('-', '_')
  • 使用 Ruby 2.3 新增的 <<~ 操作符来缩排 heredocs 中的多行文本。 [link]
# 差 - 使用 Powerpack 程序库的 String#strip_margin
code = <<-END.strip_margin('|')
  |def test
  |  some_method
  |  other_method
  |end
END

# 差
code = <<-END
def test
  some_method
  other_method
end
END

# 好
code = <<~END
  def test
    some_method
    other_method
  end
END
  • 当你不需要分组结果时,使用非捕获组。 [link]
# 差
/(first|second)/

# 好
/(?:first|second)/
  • 避免使用 Perl 风格的、用以代表最近的捕获组的特殊变量(比如 $1、$2 等)。使用 Regexp.last_match(n) 来替代。[link]
/(regexp)/ =~ string
...

# 差
process $1

# 好
process Regexp.last_match(1)
  • 小心使用 ^ 与 $ ,它们匹配的是一行的开始与结束,而不是字符串的开始与结束。如果你想要匹配整个字符串,使用 \A 与 \z。(注意,\Z 实为 /\n?\z/) [link]
string = "some injection\nusername"
string[/^username$/]   # 匹配成功
string[/\Ausername\z/] # 匹配失败
  • 对于复杂的正则表达式,使用 x 修饰符。这种做法不但可以提高可读性,而且允许你加入必要的注释。注意的是,空白字符会被忽略。 [link]
regexp = /
  start         # some text
  \s            # white space char
  (group)       # first group
  (?:alt1|alt2) # some alternation
  end
/x
  • 只有当字符串中同时存在插值与双引号,且是单行时,才使用 %()(%Q 的简写形式)。多行字符串,倾向使用 heredocs。 [link]
# 差 - 不存在插值
%(<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>)
# 应当使用 heredocs

# 好 - 同时存在插值与双引号,且是单行字符串
%(<tr><td class="name">#{name}</td>)
  • 避免使用 %q,除非字符串同时存在 ' 与 "。优先考虑更具可读性的常规字符串,除非字符串中存在大量需要转义的字符。 [link]
# 差
name = %q(Bruce Wayne)
time = %q(8 o'clock)
question = %q("What did you say?")

# 好
name = 'Bruce Wayne'
time = "8 o'clock"
question = '"What did you say?"'
quote = %q(<p class='quote'>"What did you say?"</p>)
  • 只有当正则表达式中存在一个或以上的 / 字符时,才使用 %r。 [link]
# 差
%r{\s+}

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

避免使用 method_missing。它会使你的调用栈变得凌乱;其方法不被罗列在 #methods 中;拼错的方法可能会默默地工作(nukes.launch_state = false)。优先考虑使用委托、代理、或是 define_method
来替代。如果你必须使用 method_missing 的话,务必做到以下几点: [link]

  • 确保同时定义了 respond_to_missing?
  • 仅仅捕获那些具有良好语义前缀的方法,像是 find_by_*——让你的代码愈确定愈好。
  • 在语句的最后调用 super。
  • 委托到确定的、非魔术的方法,比如:
# 差
def method_missing?(meth, *params, &block)
  if /^find_by_(?<prop>.*)/ =~ meth
    # ... 一堆处理 find_by 的代码
  else
    super
  end
end

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

# 最好的方式可能是在每个需要支持的属性被声明时,使用 define_method 定义对应的方法
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,204评论 0 4
  • 在本教程中,将探讨使用Ruby开始编程所需的基本语法,以及如何在30分钟内快速入门学习并使用Ruby编程语言。 注...
    易百教程阅读 7,138评论 1 36
  • “有谁的青春不夹杂伤与悲,誓死捍卫的随手消散成灰。” 看了昨天更新的那一集《最好的我们》,张平哭了,蒋年年哭了,耿...
    贰鹫阅读 376评论 0 1