在开发者的世界里,菲尔-卡尔顿有一句众所周知的话:There are only two hard things in Computer Science: cache invalidation and naming things 。我们通常认为这句话的意思是,很难为我们写的代码(变量、方法/函数、模块/类等)想出一个清晰、描述性和简洁的名字,但有时,我们找到的完美名字也会成为一个问题。
巨大的权力带来巨大的责任
Ruby允许我们做很多在其他语言中很难做到的事情,我们可以修改类并在运行中覆盖方法,我们可以根据其他代码的结果以编程方式定义新的类,我们可以在运行时追加/预设/扩展模块,我们可以做很多元编程...而且我们可以在Ruby解释器没有警告或投诉的情况下做到这些。
但是我们必须要小心,这有很大的权力,但是,如果使用不当,我们会产生很多问题。
保留字和保留名
这里有一个Ruby语言的一些保留词的列表。
alias
and
BEGIN
begin
break
case
class
def
defined?
do
else
elsif
END
end
ensure
false
for
if
module
next
nil
not
or
redo
rescue
retry
return
self
super
then
true
undef
unless
until
when
while
yield
__FILE__
__LINE__
虽然我们可以用其中的许多名字来定义方法,但很难将它们直接作为方法使用,而且Ruby在大多数情况下会抱怨,因为它期望围绕这些词有特定的语法。但并不总是这样!在大多数情况下,Ruby会抱怨,因为它期望围绕这些词的特定语法。也许一个叫做defined?(...) 的方法在你的应用程序中是非常有意义的,但是试图使用它实际上会调用Ruby的defined? 方法,除非你像这样使用它:self.defined?(...) 。对defined?(...) 的调用不会失败(尽管它可能会返回一个不想要的结果),而且你也不会有任何警告。
内核方法
在Ruby语法上有一些元素是我们可以覆盖的。你知道用于运行系统命令的`cmd` 语法实际上是Kernel模块的一个方法吗?
`ls`
# => shows a list of files in the current directory using the "ls" system command
但是我们可以在我们的某个模型中定义这个方法,例如。
class User
def `(cmd)
"I'm not running your command, sorry"
end
def run_ls
`ls`
end
end
User.new.run_ls
# => "I'm not running your command, sorry"
这可能会破坏很多东西......我们这样做可以有合理的理由(比如围绕执行系统命令添加自定义行为),但我们必须要小心。
`cmd` 这个例子很极端,但它只是为了说明修改那些看起来对Ruby很关键的东西是多么容易。
惯例和改变惯例
Ruby on Rails遵循 "约定高于配置"的范式,这意味着,如果我们以特定的方式命名事物,并将事物放在特定的地方,框架将施展它的 "魔法",我们将获得所有的好处。
这真的很强大,它使我们能够写更少的代码,并免费获得大量的功能。这也是Rails让构建新的应用如此简单的原因之一。
但同样,这里有一个陷阱,我们必须要小心,约定是很好的,但如果我们不了解它们,就会得到意想不到的行为。
关于约定的另一个需要注意的方面是,它们可以在不同的Rails版本之间变化。随着新功能和新配置的加入,以前有效的名字在下一个Rails版本中可能就会出现问题。
TestController和test_helper
为了说明这一点,我们将分享我们在Ruby on Rails升级过程中发现的一个问题。
我们不得不将一个Rails应用程序从Rails 5.2升级到6.0。该应用使用了MiniTest,所以一个test_helper.rb 文件被用来作为配置测试的主要点。
这个应用程序还定义了一个名为TestController 的控制器,只在测试期间使用。这个名字很清楚,没有疑义。
这在Rails 5.2中运行良好,但在Rails 6.0中产生了一个奇怪的行为:test_helper.rb 被执行了两次(导致关于已经初始化的常量的警告以及许多其他问题)。
我们发现,Rails按照Rails 6.0的惯例,根据控制器的名称自动包含了辅助文件。因此,它在项目的任何地方寻找一个名为test_helper.rb 的文件,将其包含在TestController类中,而不仅仅是在helpers 。这在Rails 5.2中是不存在的。
解决办法很简单:将TestController 改名为其他的东西,比如FakeController 。
新的名字就可以正常工作了......除非你在项目的任何地方定义了一个名为
fake_helper.rb的文件,但你知道你该怎么做。
未知重写
另一个我们需要注意名字的地方是我们如何定义方法名以及如何配置定义方法名的工具。
命名方法
有时,我们要解决的问题的领域使用了一些特定的词汇,我们希望在我们的类或方法中使用这些词汇。
例如,如果我们正在创建一个允许克隆元素的游戏,我们可能很想在我们的一个模型中添加一个clone 方法。
这个名字很有意义,它用我们正在工作的领域的语言来描述这个动作。但是我们必须要小心:clone 是一个已经被ActiveRecord定义的方法,也在Kernel模块中。如果我们不知道这一点,我们就会覆盖一个方法,而Ruby不会给我们一个提示,这是在发生。事情可能会失败,但问题将很难被发现(例如有些语言需要特定的语法来覆盖方法,以防止这些意外的覆盖,但我们没有这种un Ruby)。
我不会添加一个Rails默认在ActiveRecord对象中添加的所有方法的列表,因为这个列表真的很长,而且在不同的Rails版本中会发生变化,但这里有一个要点,以Rails 7.0中的实例方法列表为例。
生成方法的代码
在一个普通的Rails应用中,可能会有很多元编程在后台进行,根据并不明显的配置添加方法。
对于我们添加到枚举中的每个值,ActiveRecord将定义2个实例方法和2个作用域。
class Conversation < ActiveRecord::Base
enum :status, [ :active, :archived ]
end
conversation.active! # setter
conversation.active? # boolean check
Conversation.active # positive scope
Conversation.not_active # negative scope
conversation.archived!
conversation.archived?
Conversation.archived
Conversation.not_archived
但是,如果我们添加一个:frozen 状态,会发生什么呢?这将产生一个:frozen? 方法,但这已经被ActiveRecord在其内部定义了,使用的是完全不同的逻辑!
AASM gem也会引起类似的问题:如果我们定义一个frozen 状态,它将定义一个frozen? 方法,与ActiveRecord的方法相冲突。
而这与上一节有点关系。Rails随着时间的推移而变化,由AASM过度的frozen? 方法在Rails 4.1中的使用方式不同,没有出现任何问题,但在Rails 4.2中却产生了许多问题,因为Rails 4.2在新的地方使用了该方法。
同样,覆盖这些方法可能有合理的理由,但你应该小心谨慎,知道自己在做什么,并在Rails内部发生变化时做好准备。
总结
命名是一件很困难的事情,有时候,正确的名字可能是一个问题。我们在编码时不会一直考虑这个问题(每次要定义一个新方法时,检查所有这些地方都是浪费时间),但重要的是要知道这些问题可能发生,也许试着记住一些最通用的问题是个好主意。
调试一个由意外覆盖或我们使用的宝石的变化引起的问题可能真的很困难,当你遇到一个问题,在堆栈跟踪中看到一个可疑的方法名称时,也要记住这一点。