Creating an Expando object in Ruby
Is there a better way to write this Expando class? The way it is written does not work. I'm using Ruby 1.8.7
starting code quoted from https://gist.github.com/300462/3fdf51800768f2c7089a53726384350c890bc7c3
class Expando
def method_missing(method_id, *arguments)
if match = method_id.id2name.match(/(\w*)(\s*)(=)(\s*)(\.*)/)
puts match[1].to_sym # think this was supposed to be commented
self.class.class_eval{ attr_accessor match[1].to_sym }
instance_variable_set("#{match[1]}", match[5])
else
super.method_missing(meth开发者_运维知识库od_id, *arguments)
end
end
end
person = Expando.new
person.name = "Michael"
person.surname = "Erasmus"
person.age = 29
The easiest way to write it is to not write it at all! :) See the OpenStruct class, included in the standard library:
require 'ostruct'
record = OpenStruct.new
record.name = "John Smith"
record.age = 70
record.pension = 300
If I was going to write it, though, I'd do it like this:
# Access properties via methods or Hash notation
class Expando
def initialize
@properties = {}
end
def method_missing( name, *args )
name = name.to_s
if name[-1] == ?=
@properties[name[0..-2]] = args.first
else
@properties[name]
end
end
def []( key )
@properties[key]
end
def []=( key,val )
@properties[key] = val
end
end
person = Expando.new
person.name = "Michael"
person['surname'] = "Erasmus"
puts "#{person['name']} #{person.surname}"
#=> Michael Erasmus
If you're just trying to get a working Expando
for use, use OpenStruct
instead. But if you're doing this for educational value, let's fix the bugs.
The arguments to method_missing
When you call person.name = "Michael"
this is translated into a call to person.method_missing(:name=, "Michael")
, so you don't need to pull the parameter out with a regular expression. The value you're assigning is a separate parameter. Hence,
if method_id.to_s[-1,1] == "=" #the last character, as a string
name=method_id.to_s[0...-1] #everything except the last character
#as a string
#We'll come back to that class_eval line in a minute
#We'll come back to the instance_variable_set line in a minute as well.
else
super.method_missing(method_id, *arguments)
end
instance_variable_set
Instance variable names all start with the @
character. It's not just syntactic sugar, it's actually part of the name. So you need to use the following line to set the instance variable:
instance_variable_set("@#{name}", arguments[0])
(Notice also how we pulled the value we're assigning out of the arguments
array)
class_eval
self.class
refers to the Expando
class as a whole. If you define an attr_accessor
on it, then every expando will have an accessor for that attribute. I don't think that's what you want.
Rather, you need to do it inside a class << self
block (this is the singleton class or eigenclass of self
). This operates inside the eigenclass for self
.
So we would execute
class << self; attr_accessor name.to_sym ; end
However, the variable name
isn't actually accessible inside there, so we're going to need to single out the singleton class first, then run class_eval
. A common way to do this is to out this with its own method eigenclass
So we define
def eigenclass
class << self; self; end
end
and then call self.eigenclass.class_eval { attr_accessor name.to_sym }
instead)
The solution
Combine all this, and the final solution works out to
class Expando
def eigenclass
class << self; self; end
end
def method_missing(method_id, *arguments)
if method_id.to_s[-1,1] == "="
name=method_id.to_s[0...-1]
eigenclass.class_eval{ attr_accessor name.to_sym }
instance_variable_set("@#{name}", arguments[0])
else
super.method_missing(method_id, *arguments)
end
end
end
精彩评论