Marko Avlijaš Marko Avlijaš - 16 days ago 8
Ruby Question

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
.

Answer

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