如何在 Ruby 中在运行时覆盖动态创建的 setter 方法?

How to override a dynamically created setter method at runtime in Ruby?

任何人都可以帮助我通过一个与覆盖先前定义的方法有关的场景吗?

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

此外,这个 class 必须响应不同的方法,如果它们不存在,那么我们需要即时创建它们。这包括覆盖 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

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

singleton_class.send(:attr_accessor, name)

我建议使用自定义 setter:

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)

这是另一种可能的解决方法。它有点相似,但这是通过 重写 class.

中的 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