使用 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_missingbuild 提取到基础 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>