使用 re-usable 代码创建 Ruby 生成器 object
Creating Ruby builder object with re-usable code
我正在努力创建一些 Ruby 构建器 objects,并思考如何重用 Ruby 的一些魔法来减少构建器的逻辑单个 class/module。自从我上次用这门语言跳舞已经有 10 年了,所以有点生疏了。
例如,我有这个生成器:
class Person
PROPERTIES = [:name, :age]
attr_accessor(*PROPERTIES)
def initialize(**kwargs)
kwargs.each do |k, v|
self.send("#{k}=", v) if self.respond_to?(k)
end
end
def build
output = {}
PROPERTIES.each do |prop|
if self.respond_to?(prop) and !self.send(prop).nil?
value = self.send(prop)
# if value itself is a builder, evalute it
output[prop] = value.respond_to?(:build) ? value.build : value
end
end
output
end
def method_missing(m, *args, &block)
if m.to_s.start_with?("set_")
mm = m.to_s.gsub("set_", "")
if PROPERTIES.include?(mm.to_sym)
self.send("#{mm}=", *args)
return self
end
end
end
end
可以这样使用:
Person.new(name: "Joe").set_age(30).build
# => {name: "Joe", age: 30}
我希望能够将所有内容重构为 class and/or 模块,这样我就可以创建多个这样的构建器,它们只需要定义属性并继承或包含其余部分(并可能相互延伸)。
class BuilderBase
# define all/most relevant methods here for initialization,
# builder attributes and object construction
end
module BuilderHelper
# possibly throw some of the methods here for better scope access
end
class Person < BuilderBase
include BuilderHelper
PROPERTIES = [:name, :age, :email, :address]
attr_accessor(*PROPERTIES)
end
# Person.new(name: "Joe").set_age(30).set_email("joe@mail.com").set_address("NYC").build
class Server < BuilderBase
include BuilderHelper
PROPERTIES = [:cpu, :memory, :disk_space]
attr_accessor(*PROPERTIES)
end
# Server.new.set_cpu("i9").set_memory("32GB").set_disk_space("1TB").build
我已经走到这一步了:
class BuilderBase
def initialize(**kwargs)
kwargs.each do |k, v|
self.send("#{k}=", v) if self.respond_to?(k)
end
end
end
class Person < BuilderBase
PROPERTIES = [:name, :age]
attr_accessor(*PROPERTIES)
def build
...
end
def method_missing(m, *args, &block)
...
end
end
尝试将 method_missing
和 build
提取到基础 class 或模块中时,不断向我抛出错误,如:
NameError: uninitialized constant BuilderHelper::PROPERTIES
OR
NameError: uninitialized constant BuilderBase::PROPERTIES
本质上,parent class 和 mixin 都无法访问 child class' 属性。对于 parent 这是有道理的,但不确定为什么 mixin 无法读取它包含的 class 中的值。这是 Ruby 我确信有一些我错过的神奇方法可以做到这一点。
感谢帮助 - 谢谢!
我将您的样本减少到所需的部分并得出:
module Mixin
def say_mixin
puts "Mixin: Value defined in #{self.class::VALUE}"
end
end
class Parent
def say_parent
puts "Parent: Value defined in #{self.class::VALUE}"
end
end
class Child < Parent
include Mixin
VALUE = "CHILD"
end
child = Child.new
child.say_mixin
child.say_parent
这就是您如何从 parent/included class.
访问 child/including class 中的 CONSTANT
但我不明白您为什么首先要拥有整个 Builder 的东西。 OpenStruct 不适用于您的情况吗?
有趣的问题。正如@Pascal 所提到的,OpenStruct 可能已经满足您的需求。
不过,显式定义 setter 方法可能更简洁。用方法调用替换 PROPERTIES
常量也可能更清楚。因为我期望 build
方法到 return 一个完整的对象而不仅仅是一个哈希,我将它重命名为 to_h
:
class BuilderBase
def self.properties(*ps)
ps.each do |property|
attr_reader property
define_method :"set_#{property}" do |value|
instance_variable_set(:"@#{property}", value)
@hash[property] = value
self
end
end
end
def initialize(**kwargs)
@hash = {}
kwargs.each do |k, v|
self.send("set_#{k}", v) if self.respond_to?(k)
end
end
def to_h
@hash
end
end
class Person < BuilderBase
properties :name, :age, :email, :address
end
p Person.new(name: "Joe").set_age(30).set_email("joe@mail.com").set_address("NYC").to_h
# {:name=>"Joe", :age=>30, :email=>"joe@mail.com", :address=>"NYC"}
class Server < BuilderBase
properties :cpu, :memory, :disk_space
end
p Server.new.set_cpu("i9").set_memory("32GB").set_disk_space("1TB").to_h
# {:cpu=>"i9", :memory=>"32GB", :disk_space=>"1TB"}
我认为不需要声明 PROPERTIES,我们可以这样创建 general builder
:
class Builder
attr_reader :build
def initialize(clazz)
@build = clazz.new
end
def self.build(clazz, &block)
builder = Builder.new(clazz)
builder.instance_eval(&block)
builder.build
end
def set(attr, val)
@build.send("#{attr}=", val)
self
end
def method_missing(m, *args, &block)
if @build.respond_to?("#{m}=")
set(m, *args)
else
@build.send("#{m}", *args, &block)
end
self
end
def respond_to_missing?(method_name, include_private = false)
@build.respond_to?(method_name) || super
end
end
正在使用
class Test
attr_accessor :x, :y, :z
attr_reader :w, :u, :v
def set_w(val)
@w = val&.even? ? val : 0
end
def add_u(val)
@u = val if val&.odd?
end
end
test1 = Builder.build(Test) {
x 1
y 2
z 3
} # <Test:0x000055b6b0fb2888 @x=1, @y=2, @z=3>
test2 = Builder.new(Test).set(:x, 1988).set_w(6).add_u(2).build
# <Test:0x000055b6b0fb23b0 @x=1988, @w=6>
我正在努力创建一些 Ruby 构建器 objects,并思考如何重用 Ruby 的一些魔法来减少构建器的逻辑单个 class/module。自从我上次用这门语言跳舞已经有 10 年了,所以有点生疏了。
例如,我有这个生成器:
class Person
PROPERTIES = [:name, :age]
attr_accessor(*PROPERTIES)
def initialize(**kwargs)
kwargs.each do |k, v|
self.send("#{k}=", v) if self.respond_to?(k)
end
end
def build
output = {}
PROPERTIES.each do |prop|
if self.respond_to?(prop) and !self.send(prop).nil?
value = self.send(prop)
# if value itself is a builder, evalute it
output[prop] = value.respond_to?(:build) ? value.build : value
end
end
output
end
def method_missing(m, *args, &block)
if m.to_s.start_with?("set_")
mm = m.to_s.gsub("set_", "")
if PROPERTIES.include?(mm.to_sym)
self.send("#{mm}=", *args)
return self
end
end
end
end
可以这样使用:
Person.new(name: "Joe").set_age(30).build
# => {name: "Joe", age: 30}
我希望能够将所有内容重构为 class and/or 模块,这样我就可以创建多个这样的构建器,它们只需要定义属性并继承或包含其余部分(并可能相互延伸)。
class BuilderBase
# define all/most relevant methods here for initialization,
# builder attributes and object construction
end
module BuilderHelper
# possibly throw some of the methods here for better scope access
end
class Person < BuilderBase
include BuilderHelper
PROPERTIES = [:name, :age, :email, :address]
attr_accessor(*PROPERTIES)
end
# Person.new(name: "Joe").set_age(30).set_email("joe@mail.com").set_address("NYC").build
class Server < BuilderBase
include BuilderHelper
PROPERTIES = [:cpu, :memory, :disk_space]
attr_accessor(*PROPERTIES)
end
# Server.new.set_cpu("i9").set_memory("32GB").set_disk_space("1TB").build
我已经走到这一步了:
class BuilderBase
def initialize(**kwargs)
kwargs.each do |k, v|
self.send("#{k}=", v) if self.respond_to?(k)
end
end
end
class Person < BuilderBase
PROPERTIES = [:name, :age]
attr_accessor(*PROPERTIES)
def build
...
end
def method_missing(m, *args, &block)
...
end
end
尝试将 method_missing
和 build
提取到基础 class 或模块中时,不断向我抛出错误,如:
NameError: uninitialized constant BuilderHelper::PROPERTIES
OR
NameError: uninitialized constant BuilderBase::PROPERTIES
本质上,parent class 和 mixin 都无法访问 child class' 属性。对于 parent 这是有道理的,但不确定为什么 mixin 无法读取它包含的 class 中的值。这是 Ruby 我确信有一些我错过的神奇方法可以做到这一点。
感谢帮助 - 谢谢!
我将您的样本减少到所需的部分并得出:
module Mixin
def say_mixin
puts "Mixin: Value defined in #{self.class::VALUE}"
end
end
class Parent
def say_parent
puts "Parent: Value defined in #{self.class::VALUE}"
end
end
class Child < Parent
include Mixin
VALUE = "CHILD"
end
child = Child.new
child.say_mixin
child.say_parent
这就是您如何从 parent/included class.
访问 child/including class 中的 CONSTANT但我不明白您为什么首先要拥有整个 Builder 的东西。 OpenStruct 不适用于您的情况吗?
有趣的问题。正如@Pascal 所提到的,OpenStruct 可能已经满足您的需求。
不过,显式定义 setter 方法可能更简洁。用方法调用替换 PROPERTIES
常量也可能更清楚。因为我期望 build
方法到 return 一个完整的对象而不仅仅是一个哈希,我将它重命名为 to_h
:
class BuilderBase
def self.properties(*ps)
ps.each do |property|
attr_reader property
define_method :"set_#{property}" do |value|
instance_variable_set(:"@#{property}", value)
@hash[property] = value
self
end
end
end
def initialize(**kwargs)
@hash = {}
kwargs.each do |k, v|
self.send("set_#{k}", v) if self.respond_to?(k)
end
end
def to_h
@hash
end
end
class Person < BuilderBase
properties :name, :age, :email, :address
end
p Person.new(name: "Joe").set_age(30).set_email("joe@mail.com").set_address("NYC").to_h
# {:name=>"Joe", :age=>30, :email=>"joe@mail.com", :address=>"NYC"}
class Server < BuilderBase
properties :cpu, :memory, :disk_space
end
p Server.new.set_cpu("i9").set_memory("32GB").set_disk_space("1TB").to_h
# {:cpu=>"i9", :memory=>"32GB", :disk_space=>"1TB"}
我认为不需要声明 PROPERTIES,我们可以这样创建 general builder
:
class Builder
attr_reader :build
def initialize(clazz)
@build = clazz.new
end
def self.build(clazz, &block)
builder = Builder.new(clazz)
builder.instance_eval(&block)
builder.build
end
def set(attr, val)
@build.send("#{attr}=", val)
self
end
def method_missing(m, *args, &block)
if @build.respond_to?("#{m}=")
set(m, *args)
else
@build.send("#{m}", *args, &block)
end
self
end
def respond_to_missing?(method_name, include_private = false)
@build.respond_to?(method_name) || super
end
end
正在使用
class Test
attr_accessor :x, :y, :z
attr_reader :w, :u, :v
def set_w(val)
@w = val&.even? ? val : 0
end
def add_u(val)
@u = val if val&.odd?
end
end
test1 = Builder.build(Test) {
x 1
y 2
z 3
} # <Test:0x000055b6b0fb2888 @x=1, @y=2, @z=3>
test2 = Builder.new(Test).set(:x, 1988).set_w(6).add_u(2).build
# <Test:0x000055b6b0fb23b0 @x=1988, @w=6>