Search code examples
rubyinheritancemixinsbuilder

Creating Ruby builder object with re-usable code


I'm working to create a few Ruby builder objects, and thinking on how I could reuse some of Ruby's magic to reduce the logic of the builder to a single class/module. It's been ~10 years since my last dance with the language, so a bit rusty.

For example, I have this builder:

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

Which can be used like so:

Person.new(name: "Joe").set_age(30).build
# => {name: "Joe", age: 30}

I would like to be able to refactor everything to a class and/or module so that I could create multiple such builders that'll only need to define attributes and inherit or include the rest (and possibly extend each other).

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

I've been able to get this far:

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

Trying to extract method_missing and build into the base class or a module keeps throwing an error at me saying something like:

NameError: uninitialized constant BuilderHelper::PROPERTIES

OR

NameError: uninitialized constant BuilderBase::PROPERTIES

Essentially the neither the parent class nor the mixin are able to access the child class' attributes. For the parent this makes sense, but not sure why the mixin can't read the values inside the class it was included into. This being Ruby I'm sure there's some magical way to do this that I have missed.

Help appreciated - thanks!


Solution

  • I reduced your sample to the required parts and came up with:

    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
    
    

    This is how you could access a CONSTANT that lives in the child/including class from the parent/included class.

    But I don't see why you want to have this whole Builder thing in the first place. Would an OpenStruct not work for your case?