Как переопределить динамически созданный метод установки во время выполнения в Ruby? - PullRequest
0 голосов
/ 30 марта 2020

Может ли кто-нибудь еще помочь мне пройти один сценарий, связанный с переопределением ранее определенного метода?

У меня есть класс, который может получить Hash для создания переменных экземпляра во время выполнения для каждого ключа пары Если значением является Hash, то нам нужно создать экземпляр нового класса Config и присвоить его переменной экземпляра в контексте, что зафиксировано в методе initialize.

Кроме того, этот класс должен реагировать на разные методы, и если их нет, то нам нужно создавать их на лету. И это покрыто переопределением метода method_missing и оценкой, является ли значение для данного метода Hash, затем примените те же логики c, что и в инициализаторе.

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, мне нужно перехватить его и применить тот же лог c, что и раньше, создать новый объект Config с этим значением и присвоить его переменной экземпляра foo.

Проблема здесь в том, что, поскольку метод foo уже определен, я не могу перехватить его новое назначение в методе method_missing, чтобы применить необходимые логики c. Кто-нибудь знает способ перехвата, когда мы вызываем метод 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

Ответы [ 2 ]

0 голосов
/ 31 марта 2020

Это еще один возможный способ ее решения. Это немного похоже, но это работает при переопределении метода 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
0 голосов
/ 30 марта 2020

Вместо использования получателя и установщика по умолчанию:

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)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...