使用实例变量的模块可能是有害的
背景
Rails 在某种程度上鼓励人们到处使用模块和实例变量。例如,在控制器、助手(helpers)和视图中使用实例变量。他们还鼓励使用 ActiveSupport::Concern,这进一步加强了将所有内容保存在一个巨大的单一对象中的理念,人们可以访问这个巨大对象中的所有内容。
问题
当然,这开发起来很方便,因为我们伸手就能拿到所有东西。然而,当所选对象增长时,这会有很多缺点,同样的原因会导致它日后失控。
在同一上下文中有太多东西,我们不知道这些东西是否紧密耦合,是否相互依赖。当复杂度增长到一定程度时,很难判断,这使得跟踪代码也变得极其困难。例如,一个类可能使用 3 个不同的实例变量,所有这些变量都可以从 3 个不同的模块中初始化和操作。当这些变量开始给我们带来麻烦时,很难追踪。我们不知道哪个模块会突然改变其中一个变量。任何东西都可能触碰任何东西。
类似的问题
人们说多重继承是坏的。到处散布的多个模块与多个实例变量的混合存在同样的问题。ActiveSupport::Concern 也是如此。参见:
考虑用专用类和组合替换 concerns
还有一个类似的想法: 使用装饰器和接口隔离来解决模型过度增长的问题
注意 included 并不能解决整个问题。它们定义了依赖关系,但仍然允许各个模块通过最终巨大对象中的实例变量进行隐式通信,这就是问题所在。
解决方案
我们应该将巨大的对象拆分成多个对象,它们通过 API(即公共方法)相互通信。简而言之,组合优于继承。这样,每个较小的对象都会有自己各自有限的状态,即实例变量。如果一个实例变量出了问题,我们会非常清楚它来自那个单独的小对象,因为没有人能触碰它。
有了明确定义的 API,这将使事物耦合度更低,调试和跟踪更容易,并且对其他对象使用来说更具可扩展性,因为它们以清晰的方式通信,而不是隐式依赖。
可接受的使用
然而,在模块中使用实例变量并不总是坏的,只要它被限制在同一个模块中;也就是说,没有其他模块或对象在触碰它们,那么这就是可接受的使用。
我们特别允许使用单个实例变量配合 ||= 来设置值的情况。这看起来像:
module M
def f
@f ||= true
end
end不幸的是,很难将更复杂的规则编码到 cop 中,所以我们依靠人们的最佳判断。如果我们能找到另一个可以轻松添加到 cop 中的好模式,我们应该这样做。
如何重写并避免禁用此 cop
即使我们可以禁用 cop,我们也应该避免这样做。一些代码可以很容易地以简单形式重写。考虑这个可接受的方法:
module Gitlab
module Emoji
def emoji_unicode_version(name)
@emoji_unicode_versions_by_name ||=
JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'digests.json')))
@emoji_unicode_versions_by_name[name]
end
end
end这个方法完全没问题,因为它已经是自包含的。没有其他方法应该使用 @emoji_unicode_versions_by_name,我们很好。然而,它仍然违反了 cop,因为它不仅仅是 ||=,而且 cop 不够智能来判断这是可以的。
另一方面,我们可以将这个方法分成两个:
module Gitlab
module Emoji
def emoji_unicode_version(name)
emoji_unicode_versions_by_name[name]
end
private
def emoji_unicode_versions_by_name
@emoji_unicode_versions_by_name ||=
JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'digests.json')))
end
end
end现在 cop 不再抱怨了。
如何禁用此 cop
将禁用注释放在同一行代码的后面:
module M
def violating_method
@f + @g # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end如果有多行,你也可以对某个部分启用和禁用:
module M
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def violating_method
@f = 0
@g = 1
@h = 2
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end注意你需要在某个地方启用它,否则该点下面的内容不会被检查。
我们现在可能需要忽略的事情
由于 Rails 助手(helpers)和邮件发送器(mailers)的工作方式,我们可能无法避免在那里使用实例变量。对于这些情况,我们目前可以忽略它们。这些模块不与其他随机对象共享,所以它们仍然有些隔离。
视图中的实例变量
它们很糟糕,因为我们无法轻易地判断是谁在使用这些实例变量(从控制器的角度来看)以及我们在哪里设置它们(从局部模板 partials 的角度来看),这使得跟踪数据依赖变得极其困难。
我们尝试使用类似这样的东西:
= render 'projects/commits/commit', commit: commit, ref: ref, project: project在局部模板中:
- ref = local_assigns.fetch(:ref)
- commit = local_assigns.fetch(:commit)
- project = local_assigns.fetch(:project)这样能更清楚地知道这些值来自哪里,并且我们获得了使用实例变量时无法获得的拼写检查的好处。将来,我们也应该禁止在局部模板中使用实例变量。