我做了这样的构造函数:
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一样逐个手动设置它们,以及为什么。
答案 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
在我看来,这是我们所看到的最佳实施方式:
grep
,git log -S
等opts.fetch
,我们会向我们班级的未来读者清楚地说明opts
参数应该是什么样子,而不是让他们阅读整个班级。元编程有时可能有用,但这种情况很少见。作为一个粗略的指南,我更有可能在框架或库代码中使用元编程,这通常需要更通用(例如ActiveModel::AttributeAssignment
module in Rails),并在应用程序代码中避免它,这通常更多特定于特定问题或域。
即使在图书馆代码中,我也更喜欢几行重复的清晰度。
答案 1 :(得分:2)
这个问题的答案总是基于某人的个人意见所以这是我的。
如果您无法提前知道这些选项,那么您没有真正的选择,只能按照自己的意愿行事。但是,如果选项是从已知的集合中提取的,那么我倾向于清晰而简洁,并且有明确的方法来设置选项。这些也是添加任何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)