Marko Avlijaš
Marko Avlijaš

Reputation: 1659

Ruby idiom to initialize class not object

What's the standard way to initialize class (not object)?

What I did is created class_initialize method and call it.
But I am ex C# programmer. Is there a better way?

class Specs
  class << self
    def universal_properties
      [:hp, :engine_volume]
    end
    def compound_universal_properties
      [:hp_per_volume]
    end
    def convertible_properties
      [:weight, :torque]
    end
    def compound_convertible_properties
      [:weight_per_hp]
    end

    private
    def define_methods(type)
      define_method(type) { instance_variable_get("@#{type}") }
      define_method("#{type}=") do |value_and_unit|
        send(type).send(:set, value_and_unit)
      end
    end

    def class_initialize
      universal_properties.each { |p| define_methods(p) }
      convertible_properties.each { |p| define_methods(p) }
      compound_universal_properties.each { |p| define_methods(p) }
      compound_convertible_properties.each { |p| define_methods(p) }
    end
  end
  class_initialize

  public

  def initialize
    @weight = ConvertibleProperty.new(:weight)
    ...
  end
  ...
end

Less important details:
I see by first answer that this code is confusing people and this is too long for a comment.

I didn't just create attr_accessors because for example :weight and :torque are class ConvertibleProperty and have functionality like imperial.value, imperial.unit, metric.value, metric.unit, empty?...

I am calling this code like this:

specs = Specs.new
specs.weight = 800, 'kg'
specs.hp     = 300
specs.torque = 210, :metric

When I type specs.weight = 10, 'kg' ruby translates that to specs.weight=([10, 'kg']) and I don't want to replace weight with array [10, 'kg'], I want to call set method on it which stores original unit and value and provides metric and imperial function which each retuns a struct containing value and unit.

Upvotes: 0

Views: 71

Answers (2)

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

IMHO, the most idiomatic way would be to DSL this:

class Specs
  def initialize
    instance_exec(&Proc.new) if block_given?
  end
  def weight!(*args)
    weight = ...
  end
  ...
end

specs = Specs.new do 
  weight! 800, 'kg'
  hp! 300
  torque! 210, :metric
end

Other way round would be to specify proper accessors:

def torque=(*args)
  # 210, :metric
  @torque = ConvertibleProperty.new(...)
end

If the amount of variables is high, one might want to automize the creation of accessors:

PROPERTIES = {
  'UniversalProperty': [:hp, :engine_volume],
  'CompoundUniversalProperty': [:hp_per_volume],
  'ConvertibleProperty': [:weight, :torque],
  'CompoundConvertibleProperty': [:weight_per_hp]
}.freeze

PROPERTIES.each do |type, *props|
  props.each do |prop|
    attr_reader prop
    define_method "#{prop}=" do |*args|
      self.instance_variable_set(:"@#{prop}", Kernel.const_get(type).new(*args))
    end
  end
end

Upvotes: 2

user419017
user419017

Reputation:

Idiomatically, you would not create assignment methods to respond in such a way, and there isn't much value to metaprogramming here yet. I would work to write it out in simple terms first, then possibly refactor later if it becomes cumbersome to manage your different types.

Here is the idiomatic way:

class Specs
  def weight
    @weight ||= ConvertibleProperty.new(:weight)
  end

  def torque
    @torque ||= ConvertibleProperty.new(:torque)
  end

  # [..]
end

usage

specs = Specs.new
specs.weight.set(800, 'kg')
specs.torque.set(210, :metric)

Upvotes: 1

Related Questions