使用可重用代码创建Ruby构建器对象



我正在创建一些Ruby构建器对象,并思考如何重用Ruby的一些魔力,将构建器的逻辑简化为单个类/模块。我上一次用这种语言跳舞已经有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 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_missingbuild提取到基类或模块中,不断向我抛出一个错误,比如:

NameError: uninitialized constant BuilderHelper::PROPERTIES
OR
NameError: uninitialized constant BuilderBase::PROPERTIES

本质上,父类和mixin都不能访问子类的属性。对于父类来说,这是有意义的,但不确定为什么mixin不能读取它所包含的类内的值。作为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

这就是您可以从父类/包含类访问子类/包含类别中的常量的方法。

但我不明白你为什么一开始就想拥有整个Builder。OpenStruct不适用于您的情况吗?

有趣的问题。正如@Pascal所提到的,OpenStruct可能已经满足了您的需求。

不过,显式定义setter方法可能更简洁。用方法调用替换PROPERTIES常量也可能更清楚。由于我希望build方法返回一个完整的对象,而不仅仅是一个Hash,所以我将其重命名为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> 

最新更新