Question

Question: Using Ruby it is simple to add custom methods to existing classes, but how do you add custom properties? Here is an example of what I am trying to do:

myarray = Array.new();
myarray.concat([1,2,3]);
myarray._meta_ = Hash.new();      # obviously, this wont work
myarray._meta_['createdby'] = 'dreftymac';
myarray._meta_['lastupdate'] = '1993-12-12';

## desired result
puts myarray._meta_['createdby']; #=> 'dreftymac'
puts myarray.inspect()            #=> [1,2,3]

The goal is to construct the class definition in such a way that the stuff that does not work in the example above will work as expected.

Update: (clarify question) One aspect that was left out of the original question: it is also a goal to add "default values" that would ordinarily be set-up in the initialize method of the class.

Update: (why do this) Normally, it is very simple to just create a custom class that inherits from Array (or whatever built-in class you want to emulate). This question derives from some "testing-only" code and is not an attempt to ignore this generally acceptable approach.

Was it helpful?

Solution

Recall that in Ruby, you do not have access to attributes (instance variables) outside of that instance. You only have access to an instance's public methods.

You can use attr_accessor to create a method for a class that acts as a property as you describe:

irb(main):001:0> class Array
irb(main):002:1>  attr_accessor :_meta_
irb(main):003:1> end
=> nil
irb(main):004:0> 
irb(main):005:0* x = [1,2,3]
=> [1, 2, 3]
irb(main):006:0> x._meta_ = Hash.new
=> {}
irb(main):007:0> x._meta_[:key] = 'value'
=> "value"
irb(main):008:0> 

For a simple way to do a default initialization for an accessor, we'll need to basically reimplement attr_accessor ourselves:

class Class
  def attr_accessor_with_default accessor, default_value
    define_method(accessor) do
      name = "@#{accessor}"
      instance_variable_set(name, default_value) unless instance_variable_defined?(name)
      instance_variable_get(name)
    end

    define_method("#{accessor}=") do |val|
      instance_variable_set("@#{accessor}", val)
    end
  end
end

class Array
    attr_accessor_with_default :_meta_, {}
end

x = [1,2,3]
x._meta_[:key] = 'value'
p x._meta_

y = [4,5,6]
y._meta_[:foo] = 'bar'
p y._meta_

But wait! The output is incorrect:

{:key=>"value"}
{:foo=>"bar", :key=>"value"}

We've created a closure around the default value of a literal hash.

A better way might be to simply use a block:

class Class
  def attr_accessor_with_default accessor, &default_value_block
    define_method(accessor) do
      name = "@#{accessor}"
      instance_variable_set(name, default_value_block.call) unless instance_variable_defined?(name)
      instance_variable_get(name)
    end

    define_method("#{accessor}=") do |val|
      instance_variable_set("@#{accessor}", val)
    end
  end
end

class Array
    attr_accessor_with_default :_meta_ do Hash.new end
end

x = [1,2,3]
x._meta_[:key] = 'value'
p x._meta_

y = [4,5,6]
y._meta_[:foo] = 'bar'
p y._meta_

Now the output is correct because Hash.new is called every time the default value is retrieved, as opposed to reusing the same literal hash every time.

{:key=>"value"}
{:foo=>"bar"}

OTHER TIPS

Isn't a property just a getter and a setter? If so, couldn't you just do:

class Array
  # Define the setter
  def _meta_=(value)
    @_meta_ = value
  end

  # Define the getter
  def _meta_
    @_meta_
  end
end

Then, you can do:

x = Array.new
x._meta_
# => nil

x._meta_ = {:name => 'Bob'}

x._meta_
# => {:name => 'Bob'}

Does that help?

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top