【问题标题】:How to override a dynamically created setter method at runtime in Ruby?如何在 Ruby 运行时覆盖动态创建的 setter 方法?
【发布时间】:2020-03-30 07:31:00
【问题描述】:

其他人可以帮我解决一个与覆盖先前定义的方法有关的场景吗?

我有一个类可以接收Hash,以便在运行时为每个键值对创建实例变量。如果值是Hash,那么我们需要实例化一个新的Config 类并将其分配给上下文中的实例变量,这在initialize 方法中是固定的。

此外,这个类必须响应不同的方法,如果它们不存在,那么我们需要动态创建它们。这可以通过覆盖 method_missing 方法并评估给定方法的值是否为 Hash 来解决,然后应用与初始化程序中相同的逻辑。

class Config
  def initialize(parameters = {})
    raise ArgumentError.new unless parameters.is_a?(Hash)
    parameters.each do |key, value|
      raise ArgumentError.new unless key.is_a?(String) || key.is_a?(Symbol)
      create_new_method_on_instance(key, value)
    end
  end

  def method_missing(method_name, *args)
    name = method_name.to_s.delete_suffix('=')
    create_new_method_on_instance(name, args.first)
  end

  private

  def create_new_method_on_instance(name, value)
    singleton_class.send(:attr_accessor, name)
    if value.is_a?(Hash)
      instance_variable_set("@#{name}", Config.new(value))
    else
      instance_variable_set("@#{name}", value)
    end
  end
end

一切正常,但问题是现在,我需要即时覆盖foo 方法。例如,首先创建一个Config.new({foo => 23}) 对象,该对象将具有一个foo 实例变量,然后我想传递一个新值(重新分配它),例如config.foo = {x: 23}。 由于这个新值是hash,所以我需要截取它并应用与以前相同的逻辑,使用该值创建一个新的Config 对象并将其分配给foo 实例变量。

这里的问题是,由于foo 方法已经定义,我无法在method_missing 方法中截取它的新赋值来应用所需的逻辑。 当我们动态调用 setter 方法时,有人知道如何拦截吗?

测试:

describe 'VerifiedConfig' do
  it 'should return nil for non-existing config values' do
    config = Config.new

    expect(config.foo).to be_nil
    expect(config.bar).to be_nil
  end

  it 'should allow assigning new simple config values' do
    config = Config.new

    config.foo = 13
    config.bar = "foo-bar"

    expect(config.foo).to eq(13)
    expect(config.bar).to eq("foo-bar")
  end

  it 'should allow assigning hash values' do
    config = Config.new

    config.foo = {bar: {'baz' => 'x'}}
    config.bar = {'foo' => {bar: [12, 13], baz: 14}}

    expect(config.foo).to be_a(Config)
    expect(config.foo.bar).to be_a(Config)
    expect(config.foo.bar.baz).to eq('x')
    expect(config.bar.foo.bar).to eq([12, 13])
    expect(config.bar.foo.baz).to eq(14)
  end

  it 'should allow initialization through constructor' do
    config = Config.new({'foo' => {bar: [12, 13], baz: 14}})

    expect(config.foo.bar).to eq([12, 13])
    expect(config.foo.baz).to eq(14)
  end

  it 'should override values' do
    config = Config.new({'foo' => {bar: 'baz'}})

    config.foo = 10
    config.foo = {x: {y: 'z'}}

    expect(config.foo.x.y).to eq('z')
  end

  it 'should raise an error when keys have illegal type' do
    config = Config.new

    expect {config.x = {14 => 15}}.to raise_error(ArgumentError)
  end

  it 'should not accept anything that Hash in the constructor' do
    expect {Config.new(11)}.to raise_error(ArgumentError)
    expect {Config.new('test')}.to raise_error(ArgumentError)
  end
end

这是失败的场景:

it 'should override values' do
  config = Config.new({'foo' => {bar: 'baz'}})

  config.foo = 10
  config.foo = {x: {y: 'z'}}

  expect(config.foo.x.y).to eq('z')
end

注意:我不能使用OpenStruct

【问题讨论】:

  • 为什么不能使用OpenStruct? ...
  • 我在您的代码中没有看到任何覆盖。
  • @TomLord 因为这是对锻炼的限制!
  • @JörgWMittag method_missing 被覆盖了,你不这么认为吗?

标签: ruby hash metaprogramming dynamic-programming


【解决方案1】:

而不是使用默认的getter和setter:

singleton_class.send(:attr_accessor, name)

我建议使用自定义设置器:

singleton_class.send(:attr_reader, name)
define_singleton_method("#{name}=") do |value|
  if value.is_a?(Hash)
    instance_variable_set("@#{name}", Config.new(value))
  else
    instance_variable_set("@#{name}", value)
  end
end
public_send("#{name}=", value)

【讨论】:

  • 谢谢,它就像一个魅力!你是对的,我应该重写 setters 方法中的逻辑,这样就可以在调用它时执行它,并在那里决定如何处理要分配的值。很好的答案!
【解决方案2】:

这是另一种可能的解决方法。这有点相似,但这是通过覆盖类中的attr_accessor 方法来实现的。

我在发布这个问题后找到了它,它也很好用:

class Config
  def initialize(parameters = {})
    raise ArgumentError.new unless parameters.is_a?(Hash)

    parameters.each do |key, value|
      raise ArgumentError.new unless key.is_a?(String) || key.is_a?(Symbol)

      create_accessor_methods_and_assign_value(key, value)
    end
  end

  def method_missing(method_name, *args)
    name = method_name.to_s.delete_suffix('=')
    create_accessor_methods_and_assign_value(name, args.first)
  end

  private

  def create_accessor_methods_and_assign_value(name, value)
    singleton_class.send(:attr_accessor, name)
    public_send("#{name}=", value)
  end

  def self.attr_accessor(*names)
    names.each do |name|
      # Create Getter method
      define_method(name) { instance_variable_get("@#{name}") }
      # Create Setter method
      define_method("#{name}=") do |arg|
        if arg.is_a?(Hash)
          instance_variable_set("@#{name}", Config.new(arg))
        else
          instance_variable_set("@#{name}", arg)
        end
      end
    end
  end
end

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-08-05
    • 2011-10-15
    • 2016-04-03
    • 2014-08-13
    • 1970-01-01
    • 1970-01-01
    • 2012-05-14
    • 1970-01-01
    相关资源
    最近更新 更多