初学者应该知道的Ruby最佳实践

444 阅读11分钟

在谈到最适合编程新手的语言时,Python的统治地位从未受到真正的质疑,因为它几乎满足了定义简单语言的所有条件。它非常容易上手,可以应对任何挑战。但是Ruby呢?

虽然它没有得到足够的赞誉,但Ruby对于初学者来说是一种了不起的语言。它提供了强大的结构(如块)和通用的概念(如类似Smalltalk的消息传递),但保留了Python的流畅和优雅的英语式语法。事实上,在许多情况下,人们可以争辩说,Ruby语法的独特设计选择在可读性和表现力方面甚至超过了Python。如果你正在接触编程,很容易推荐你使用Ruby。

本文旨在建立一些被Ruby编程社区普遍接受的做法。在我们直接讨论你作为一个初学者如何引导Ruby的力量之前,让我提一下你应该牢记的两个日语短语。*"okonomi ""omakase"。*我保证这是有意义的,你将会看到为什么。

"okonomi"

Ruby一直被 "为程序员的幸福而优化 "的原则所驱动,因此,它总是提供几种不同的方法来做同一件事,而不像Python,它重视 "有一种,最好是只有一种方法来做一件事 "的哲学。因此,许多从其他语言来到Ruby的人发现他们对该语言为完成任何特定任务提供的大量选择感到困惑。

这就是日本人所说的*"okonomi",*一个通常适用于寿司的短语,翻译为 "我会选择要点什么"。

语句是表达式

在Ruby中,不存在语句。每一行和每一条命令都是一个表达式,它的评估结果是什么。即使是函数定义也会返回一些东西,即符号化的函数名称。

因此,虽然你不能在变量中保存函数(这是Python和JavaScript所允许的),但你可以将它们的标识符作为符号保存,并通过send 来调用它们(关于这一点,我们稍后再谈):

name = 'DeepSource'     # returns the string 'DeepSource' itself...
b = name = 'DeepSource' # ...so this is also valid

puts name               # displays "DeepSource" and returns nil

name.split('').select do |char|
  char.downcase == char # block returns true or false
end                     # returns ["e", "e", "p", "o", "u", "r", "c", "e"]

def prime? num
    # ...
end                     # returns :prime?

你怎样才能充分利用这一点呢?这个特殊的设计决定导致了一些值得注意的特点,其中最容易识别的是returnnext 这两个关键字,在大多数情况下是多余的,建议不要使用它们,除非你想提前终止函数或块:

def prime? num
  if num >= 2
    f = (2...num).select do |i|
      ~~next~~ num % i == 0
    end
    ~~return~~ f.size == 0
  else
    ~~return~~ false
  end
end

再见小括号

在Ruby函数中,小括号在某些注意事项下也是可选的。这使得Ruby成为定义特定领域语言或DSL的理想选择。特定领域的语言是建立在另一种语言之上的语言,它为特定的专门目的定义了抽象的内容。

对于DSL的例子,你不需要再去看Rails或RSpec,但为了简单起见,考虑一下Sinatra,一个用Ruby构建的简单的Web服务器。这是一个用Sinatra构建的简单服务的样子:

require 'sinatra'

get '/' do
  'Hello, World!'
end

从这个例子中不能立即看出的是,这里的get ,看起来非常像一个语言关键词,实际上是一个函数!这里的get 函数需要两个参数,一个是字符串路径,一个是包含路径匹配时要执行的代码的块。该块的返回值将是与方法和路径相匹配的请求的HTTP响应。

你如何最大限度地利用这一点?通过拥抱这种自由,你可以为你的用例定义你自己的抽象!Ruby积极鼓励开发者为日常的抽象定义他们自己的小DSL,并通过使用它们来编写干净的代码。最重要的是,这些代码读起来像伪代码,这总是一件好事。

虽然小括号是可选的,但惯例是一般只对零或一个参数省略小括号,而当参数超过一个时则保留小括号,以利于明确:

require 'date'

def log message
  timestamp = DateTime.now.iso8601
    puts "[#{timestamp}] #{message}" 
end

log "Hello World!"
# [2020-12-30T21:18:30+05:30] Hello World!

方法名?

Ruby允许方法名有各种各样的标点符号,如=,?,! 。根据惯例,这些标点符号都有其作用,并表明特定的函数的性质:

| Punctuation | Purpose                                       | Example   |
|-------------|-----------------------------------------------|-----------|
| `?`         | Returns a Boolean value, `true` or `false`    | .nil?     |
| `!`         | Modifies values in place or raises exceptions | .reverse! |

许多Ruby函数有两种变体,常规的和 "砰 "的。为了理解其中的区别,请考虑这个例子,在字符串上的reverse 方法的两个变体:

str = 'DeepSource'

str.reverse  # "ecruoSpeeD"
str          # "DeepSource"

str.reverse! # "ecruoSpeeD"
str          # "ecruoSpeeD"

在Ruby中,名称中带有感叹号的函数会修改它们所调用的对象。在Rails中,感叹号表示,如果函数不能完成上述任务,它将引发一个异常,而不是像它的非bang对应函数那样无声地失败。

你如何充分利用这一点呢?你应该在你的代码中采用同样的惯例,这样代码的使用者就可以很容易地了解到函数的性质了。

符号高于字符串

符号的概念是Ruby独有的东西。符号只是内存中的字符串,并不是每次定义时都要分配新的内存:

'text'.object_id # 200
'text'.object_id # 220

:text.object_id  # 2028508
:text.object_id  # 2028508

符号在Ruby中被大量使用,特别是作为哈希和定义标识符和函数的常量的键。

你可以用两种方式来符号化一个字符串,在字符串前加冒号: (除非字符串是一个有效的标识符,否则需要加引号),或者对其调用to_sym 方法' :

:identifier                # :identifier
:'not an identifier'       # :"not an identifier"
'not_an_identifier'.to_sym # :"not an identifier"

**我如何充分利用这一点呢?**在Ruby的哈希值中,键和值一般是用火箭操作符=> 。从1.9版本开始,Ruby也提供了一种更简洁、更干净、类似于JavaScript的语法,使用冒号: ,但前提是你的键是符号。

person_one = { 'name' => 'Alice', 'age' => 1 }
person_one['name'] # 'Alice'

person_two = { :name => 'Bob', :age => 2 } # Replace strings with symbols
person_two[:name]  # 'Bob'

person_three = { name: 'Carol', age: 3 } # Use new style
person_three[:name]  # 'Carol'

第二种符号更容易阅读,对于较大的数据量表现更好,现在被广泛推荐为定义哈希的首选方式。符号也被用于向对象传递消息,使用send !)。

谈论消息

Ruby从Smalltalk中继承了消息传递的概念,并全身心地投入到这个理念中。Ruby中的每个方法调用和每个变量访问都是通过向实例发送消息和接收响应来实现的。

消息的发送是通过使用send 函数(或public_send ,以避免绕过可见性),并将符号与你想调用的方法的名称以及该函数可能需要的任何参数一起传递。Ruby还允许你使用define_method (咄!)动态地定义方法,甚至在一个对象上没有定义方法时执行动作:

str = "DeepSource"

str.respond_to? :reverse # true
str.send(:reverse)       # "ecruoSpeeD"

str.respond_to? :[]      # true
str.send(:[], 4..9)      # "Source"

str.respond_to? :dummy   # false

上面的例子与你通过调用字符串的reversesplit 方法所能实现的是一样的。如果一个类对一个消息做出反应,它就会执行动作,如果不执行就会引发一个异常。

**我如何充分利用这个功能呢?**玩弄这种消息功能可以开启一个全新的元编程世界,这是其他语言所不能比拟的。你应该尝试使用这些功能,从你的代码中去除模板,写出:

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end

  [:name, :age].each do |attr|
        # Get getters for @name and @age with logging
    define_method attr do
      val = instance_variable_get("@#{attr}".to_sym)
      puts "Accessing `#{attr}` with value #{val}"
      val
    end
  end

  # Handle missing attribute
    puts "Person does not #{m.id2name}"
  end
end

person = Person.new("Dhruv", 24)
person.name # "Dhruv" | prints "Accessing `name` with value Dhruv"
person.age  # 24      | prints "Accessing `age` with value 24
person.sing # nil     | prints "Person does not sing" 

猴子修补

这也许是最有争议的,但也是Ruby中最强大的功能。

Monkey patching是对 "guerrilla patching "和 "monkeying about "这两个短语的戏称,它是打开预定义的类并改变其现有功能或为其添加功能的过程。作为一种语言,Ruby是仅有的几个积极鼓励使用 "猴子打补丁 "的语言之一,将其作为向语言添加功能的一种合法方式。

当像Python和JavaScript这样的语言不鼓励这个过程时,Ruby拥抱它,并使它非常容易扩展任何类或模块,就像这样简单。

class String
  def palindrome?
    self == reverse
  end
end

'racecar'.palindrome? # true
'arizona'.palindrome? # false

**我怎样才能最大限度地利用这个?**像Rails这样的框架积极使用猴子补丁来为内置的Ruby类添加功能,如StringInteger 。如果仔细使用并适当记录,这是一种在一个地方添加功能并使其在整个代码库中到处可用的强大方式。

应该注意不要过度使用,只在根类中添加最通用的代码。

为了证明这一点,请欣赏Rails ActiveSupport的猴子补丁代码之美。你可以从Rails中单独安装它,并在其文档中看到一个全面的例子列表。

time = '2021-01-01 12:00:00'.to_time        # 2020-01-01 13:00:00 +0530
day_prior = time - 1.day                    # 2019-12-31 13:00:00 +0530
almost_new_year = day_prior.end_of_day      # 2019-12-31 23:59:59 +0530
happy_new_year = almost_new_year + 1.second # 2020-01-01 00:00:00 +0530
1.in? [1, 2, 3]                                                    # true

{ sym_key: 'value' }.with_indifferent_access[:sym_key.to_s]        # "value"
{ 'str_key' => 'value' }.with_indifferent_access['str_key'.to_sym] # "value"

少即是多

为了追求程序员的幸福,Ruby被语法糖和方法别名塞得满满的,重点在于可读性。

**我怎样才能最大限度地利用这些呢?**你最好在Ruby语法允许的情况下使用速记,因为减少的标点和增加的简洁性使代码更容易阅读和精神处理。

想做一个字符串、符号或数字的数组?使用% 符号。

%w[one two three] # ["one", "two", "three"]
%i[one two three] # [:one, :two, :three]

这些速记法有大写版本,也允许插值。

pfx = 'item'
%W[#{pfx}_one #{pfx}_two #{pfx}_three] # ["item_one", "item_two", "item_three"]
%I[#{pfx}_one #{pfx}_two #{pfx}_three] # [:item_one, :item_two, :item_three]

另一个有趣的速记符号是当你试图在一个数组上进行映射或选择,而你想做的只是在所有对象上调用一个方法。

strings = %w[one two three]
strings.map &:upcase # ~ strings.map { |str| str.upcase! }

在编程语言中,许多我们认为理所当然的操作,如数学运算,都是语法糖的符号由于Ruby是建立在消息传递的基础上,任何有效的字符串都是有效的方法名称,如+[]

1.+(2)           # ~ 1 + 2

words = %w[zero one two three four]
words.[](2)      # ~ words[2]
words.<<('five') # ~ words << 2

"Omakase"

还记得我们开始时我让你记住的另一个词吗?我保证它是相关的。*"Omakase ""okonomi "*餐的一个相反版本,这次是由厨师为你挑选的寿司组成。虽然Ruby提供了多种方法来完成某件事情,但它也有自己的缺点,尤其是在涉及到一致性和代码审查时。

def is_prime(num)
  if num >= 2
    f = (2...num).select do |i|
      next num % i == 0
    end
    return f.size == 0
  else
    return false
  end
end

is_prime(0) # false
is_prime(1) # false
is_prime(2) # true
is_prime(3) # true
is_prime(4) # false
class Integer
  def prime?
    return false if self < 2

    (2...self).none? { |i| self % i == 0 }
  end
end

0.prime? # false
1.prime? # false
2.prime? # true
3.prime? # true
4.prime? # false

根据我们所了解的情况,我们可以对代码做如下修改。

  • monkey patch方法定义到Integer 类中
  • 在有布尔返回类型的方法名中使用?
  • 删除多余的括号( )
  • 提前退出,而不是将整个代码缩进if 块内
  • 删除多余的returnnext 关键字
  • 使用大括号内嵌一个表达式的块{ }
  • 使用Ruby的大量内置函数,比如说Array::none?

在开始使用任何一种新的语言进行编程时,使用一个全面的风格指南和linter是关键。它不仅能提高你的代码质量,而且还能在你编码时实时指出改进之处,为你提供一个很好的学习方法。

腹腔镜会减少你编写程序的方法,但你在表达的创造性方面的损失在不一致性和代码的清晰度方面得到了更大的补偿。即使是作为一个专业人员,我也很乐意做出这样的权衡,如果我是一个新手,就更应该如此。

让我推荐两个你作为初学者应该使用的工具。

RuboCop

说到Ruby的linter,没有什么能与RuboCop相提并论。它是一个集linter、格式化和风格指南于一体的工具,虽然它是可配置的,但开箱即用的配置足以让你在正确的轨道上保证代码质量。RuboCop还可以帮助你扫描安全漏洞,让你有另一个理由来设置它。

今天就安装并试用它吧。你未来的自己会感谢你的,可能还有我。

当谈到在一个团队中工作时,审查其他人的代码变得很重要。DeepSource是一个自动化的代码审查工具,它可以管理端到端的代码扫描过程,并在新的提交被推送,或针对主分支开启新的拉取请求时,自动提出修复请求。

为Ruby设置DeepSource是非常容易的。只要你设置了它,它就会扫描你的整个代码库,找到改进的范围,修复它们,并为这些修改打开PR。

这就是全部了,伙计们祝大家学习Ruby愉快,它是一种神奇的语言。