使用可重用代码创建Ruby builder对象

使用可重用代码创建Ruby builder对象,ruby,inheritance,mixins,builder,Ruby,Inheritance,Mixins,Builder,我正在创建一些Ruby builder对象,并考虑如何重用Ruby的一些魔力,将生成器的逻辑简化为单个类/模块。自从我最后一次用这种语言跳舞以来已经10年了,所以有点生疏了 例如,我有一个生成器: 班级人员 属性=[:名称,:年龄] 属性存取器(*属性) def初始化(**kwargs) kwargs.每个do | k,v| self.send(“#{k}=”,v)如果self.response_to?(k) 结束 结束 def构建 输出={} 属性。每个do | prop| 如果自我。回应?(

我正在创建一些Ruby builder对象,并考虑如何重用Ruby的一些魔力,将生成器的逻辑简化为单个类/模块。自从我最后一次用这种语言跳舞以来已经10年了,所以有点生疏了

例如,我有一个生成器:

班级人员
属性=[:名称,:年龄]
属性存取器(*属性)
def初始化(**kwargs)
kwargs.每个do | k,v|
self.send(“#{k}=”,v)如果self.response_to?(k)
结束
结束
def构建
输出={}
属性。每个do | prop|
如果自我。回应?(道具)和!自我发送(道具)。零?
值=self.send(prop)
#如果值本身是一个生成器,则对其进行求值
输出[prop]=值。是否响应?(:构建)?价值。构建:价值
结束
结束
输出
结束
def方法_缺失(m、*参数和块)
如果m.to_.以?(“set_”)开头
mm=m.to_.s.gsub(“设置”
如果属性。包括?(毫米到毫米)
self.send(“#{mm}=”,*args)
回归自我
结束
结束
结束
结束
可以这样使用:

Person.new(名字:“Joe”)。设置年龄(30岁)。构建
#=>{姓名:“乔”,年龄:30}
我希望能够将所有内容重构为一个类和/或模块,这样我就可以创建多个这样的构建器,这些构建器只需要定义属性并继承或包含其余部分(并可能相互扩展)

本质上,父类和mixin都不能访问子类的属性。对于父级来说,这是有意义的,但不确定为什么mixin无法读取它所包含的类中的值。这是Ruby,我相信有一些神奇的方法可以做到这一点,但我错过了


谢谢你的帮助

我将您的样品缩小到所需的部分,并得出:

模块混入
def say_mixin
放置“Mixin:在#{self.class::Value}中定义的值”
结束
结束
班级家长
父母
放置“父:在#{self.class::Value}中定义的值”
结束
结束
类子<父
包括Mixin
VALUE=“CHILD”
结束
child=child.new
孩子,说吧
孩子,比如说父母
这就是如何从父类/包含类访问存在于子类/包含类中的常量的方法


但我不明白你为什么一开始就想拥有整个建筑商的东西。OpenStruct对您的案例不起作用吗?

有趣的问题。正如@Pascal所提到的,OpenStruct可能已经完成了您想要的功能

不过,显式定义setter方法可能更简洁。用方法调用替换
属性
常量可能更清晰。由于我希望
build
方法返回完整的对象,而不仅仅是散列,所以我将其重命名为
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"}
类构建器基
定义自身属性(*ps)
ps.每个do |属性|
属性读取器属性
define_方法:“set#{property}”do|值|
实例变量集(:“@#{property}”,值)
@哈希[属性]=值
自己
结束
结束
结束
def初始化(**kwargs)
@哈希={}
kwargs.每个do | k,v|
self.send(“set#{k}”,v)如果self.respond_to?(k)
结束
结束
def to_h
@散列
结束
结束
类人“乔”,“年龄=>30岁,:电子邮件=>”joe@mail.com“,:地址=>“纽约”}
类服务器“i9”,:内存=>“32GB”,:磁盘空间=>“1TB”}

我认为不需要声明属性,我们可以创建一个
通用生成器,如下所示:

类生成器
属性读取器:构建
def初始化(clazz)
@build=clazz.new
结束
定义自构建(clazz和block)
builder=builder.new(clazz)
builder.instance_eval(&block)
建筑商
结束
def设置(属性、值)
@build.send(“#{attr}=”,val)
自己
结束
def方法_缺失(m、*参数和块)
如果@build.response_to?(“#{m}=”)
集合(m,*args)
其他的
@build.send(“#{m}”,*args和block)
结束
自己
结束
def respond_to_missing?(方法名称,include_private=false)
@build.response_to?(方法名称)| | super
结束
结束
使用

类测试
属性存取器:x,:y,:z
属性读取器:w,:u,:v
def设置w(val)
@w=val和偶数?瓦尔:0
结束
def添加(val)
@如果val为奇数,则u=val?
结束
结束
test1=Builder.build(测试){
x1
y 2
z3
} # 
test2=Builder.new(Test).set(:x,1988).set_w(6).add_(2).build
#  

此策略的优势是什么?您仍然需要手动定义方法。@EricDuminil否,我将委托给
clazz
Test
,在我的示例中),我不需要定义任何新方法。您仍然需要手动定义
set\w
add\u
,不是吗
Builder
可以自动化setter定义。不,两种方法
set\u w
add\u
都属于
Test
类(仅用于设置/获取某些属性读取器)。我的目标是关注问题的需求:
general
并假设
Builder
client
是开发人员,这意味着他们知道如何构建特定类的实例,因此我的Builder只是一个方便的工具。例如,现在您有了另一个
Test1
类,它只有public:a,:b,:c,您可以构建`Builder.new(Test1){a1b2c3}`或
Builder.new(Test1).set(:a,1).set(:b,2).set(:c,3).build
好的。我可能误解了这些要求,但OP显然想得到
set\u name
set\u memory
。。。自动地,只定义属性,而不定义方法。我在回答中写了一个建议,让我们看看OP对我们的实现有何看法。
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
NameError: uninitialized constant BuilderHelper::PROPERTIES

OR

NameError: uninitialized constant BuilderBase::PROPERTIES
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"}