如何在 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
任何人都可以帮助我通过一个与覆盖先前定义的方法有关的场景吗?
我有一个 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