构造函数中的instance_variable_set

时间:2016-02-15 14:44:33

标签: ruby

我做了这样的构造函数:

class Foo
  def initialize(p1, p2, opts={})
    #...Initialize p1 and p2
    opts.each do |k, v|
      instance_variable_set("@#{k}", v)
    end
  end
end

我想知道动态设置这样的实例变量是不是一个好习惯,或者我应该像大多数lib一样逐个手动设置它们,以及为什么。

4 个答案:

答案 0 :(得分:4)

诊断问题

你在这里做的是元编程的一个相当简单的例子,即基于某些输入动态生成代码。元编程通常会减少您需要编写的代码量,但会使代码更难理解。

在这种特殊情况下,它还引入了一些耦合问题:类的公共接口与内部状态直接相关,这种方式很难在不改变另一个的情况下更改内部状态。

重构示例

考虑一个稍长的例子,我们使用其中一个实例变量:

class Foo
  def initialize(opts={})
    opts.each do |k, v|
      instance_variable_set("@#{k}", v)
    end
  end

  def greet(name)
    greeting = @greeting || "Hello"
    puts "#{greeting}, name"
  end
end

Foo.new(greeting: "Hi").greet

在这种情况下,如果有人想将@greeting实例变量重命名为其他内容,他们可能很难理解如何执行此操作。很明显@greeting方法使用了greet,但搜索@greeting代码并不能帮助他们找到首次设置的位置。更糟糕的是,为了改变这一内部状态,他们还必须改变对Foo.new的任何调用,因为我们采用的方法将内部状态与公共接口联系起来。

删除元编程

让我们看一个替代方案,我们只存储所有opts并将其视为状态:

class Foo
  def initialize(opts={})
    @opts = opts
  end

  def greet(name)
    greeting = @opts.fetch(:greeting, "Hello")
    puts "#{greeting}, name"
  end
end

Foo.new(greeting: "Hi").greet

通过删除元编程,这可以略微澄清情况。希望第一次更改此代码的新团队成员将会稍微更轻松一些,因为他们可以使用编辑器功能(如查找和替换)来重命名内部ivars,以及传递给初始化器的参数与内部状态之间的关系更加明确。

减少耦合

我们可以更进一步,并将内部与界面分离:

class Foo
  def initialize(opts={})
    @greeting = opts.fetch(:greeting, "Hello")
  end

  def greet(name)
    puts "#{@greeting}, name"
  end
end

Foo.new(greeting: "Hi").greet

在我看来,这是我们所看到的最佳实施方式:

  1. 没有元编程,这意味着我们可以找到对设置和使用的变量的显式引用,例如:使用编辑器的搜索功能grepgit log -S
  2. 我们可以在不改变界面的情况下改变类的内部,反之亦然。
  3. 通过在初始化程序中调用opts.fetch,我们会向我们班级的未来读者清楚地说明opts参数应该是什么样子,而不是让他们阅读整个班级。
  4. 何时使用元编程

    元编程有时可能有用,但这种情况很少见。作为一个粗略的指南,我更有可能在框架或库代码中使用元编程,这通常需要更通用(例如ActiveModel::AttributeAssignment module in Rails),并在应用程序代码中避免它,这通常更多特定于特定问题或域。

    即使在图书馆代码中,我也更喜欢几行重复的清晰度。

答案 1 :(得分:2)

这个问题的答案总是基于某人的个人意见所以这是我的。

Clarity v Brevity

如果您无法提前知道这些选项,那么您没有真正的选择,只能按照自己的意愿行事。但是,如果选项是从已知的集合中提取的,那么我倾向于清晰而简洁,并且有明确的方法来设置选项。这些也是添加任何rdoc等的好地方。

安全

从安全角度来看,拥有处理选项设置的方法可以让您根据需要执行验证。

答案 2 :(得分:1)

当您需要执行此类操作时,参数的清单会有所不同。在这种情况下,Ruby(以及大多数现代语言)中已经有了一个方便的结构:数组和哈希。在这种情况下,您应该将整个选项保存为单个哈希。这会使事情变得更简单。

答案 3 :(得分:1)

您可以使用attr_accessor声明可用的实例变量并动态调用setter,而不是动态创建实例变量:

class Foo
  attr_accessor :bar, :baz, :qux

  def initialize(opts = {})
    opts.each do |k, v|
      public_send("#{k}=", v)
    end
  end
end

Foo.new(bar: 1, baz: 2) #=> #<Foo:0x007fa8250a31e0 @bar=1, @baz=2>
Foo.new(qux: 3)         #=> #<Foo:0x007facbc06ed50 @qux=3>

如果传递了未知选项,此方法也会显示错误:

Foo.new(quux: 4) #=> undefined method `quux=' for #<Foo:0x007fd71483aa20> (NoMethodError)