Movie.new(:title => 'Godfather', :subtitles => 'English',
:audio_track_1 => "English", :audio_track_2 => "French")
I'm sure that doesn't look very surprising if you are familiar with creating ActiveRecord::Base (AR::B) subclasses; however, my current project has no database and the Movie class was not a subclass of AR::B. Additionally, the Movie instance needed readers for title, subtitles, audio_track_1, and audio_track_2, but the instance is immutable so writers are not necessary. Lastly, I wanted my Movie instances to be able to have any attribute that the hash passes in as a key.
Since I needed the attributes to be dynamic based on the hash I started with a loop that defined methods on the instance.
; class << self; self; end end
end
metaclass.class_eval do
options.each_pair do |key, value|
define_method key do
value
end
end
end
end
end
movie = Movie.new(:title => 'Godfather', :subtitles => 'English')
movie.title # => "Godfather"
movie.subtitles # => "English"
The above code works fine, and the constructor logic could be abstracted and defined in Class instead to make the Movie definition cleaner; however, I thought it would be nice to create a solution that didn't need to class_eval on the metaclass. Extending a module is another easy way to define additional behavior on an object, so I put together the following code that converts a hash into a module that has a method defined for each key and the return value of the method is the associated value.
hash = self
Module.new do
hash.each_pair do |key, value|
define_method key do
value
end
end
end
end
end
With the Hash#to_mod method available the constructor of Movie simply extends the new instance with the module created by the hash.
self.extend options.to_mod
end
end
movie = Movie.new(:title => 'Godfather', :subtitles => 'English')
movie.title # => "Godfather"
movie.subtitles # => "English"
Pat Farley pointed out that this solution is also nice because it allows me to avoid costly typos by converting my Hash instances into an object with methods. For example, If I have a params hash in a controller I can easily create an object and access it's methods instead of using keys with the hash.
# ...
end
end
params # => {:person=>{:name=>"Pat Farley", :blog=>"http://www.klankboomklang.com/"}}
person = Object.new.extend(params[:person].to_mod)
PersonService.process(person.name, person.blog)
end
end
Of course, Pat also pointed out that Hash#to_obj could be defined to make things look even a bit nicer.
hash = self
Module.new do
hash.each_pair do |key, value|
define_method key do
value
end
end
end
end
Object.new.extend self.to_mod
end
end
# ...
end
end
params # => {:person=>{:blog=>"http://www.klankboomklang.com/", :name=>"Pat Farley"}}
person = params[:person].to_obj
PersonService.process(person.name, person.blog)
end
end
Lastly, Pat recommended a recursive version that would allow you to access the entire hash by way of method calls.
self
end
end
hash = self
Module.new do
hash.each_pair do |key, value|
define_method key do
value.to_obj
end
end
end
end
Object.new.extend self.to_mod
end
end
name # => "Pat Farley"
blog # => "http://www.klankboomklang.com/"
end
end
params # => {:person=>{:blog=>"http://www.klankboomklang.com/", :name=>"Pat Farley"}}
PersonService.process(params.to_obj.person.name, params.to_obj.person.blog)
end
end
Jay,
ReplyDeleteCheck out the small constructor.rb library at http://atomicobjectrb.rubyforge.org
Pretty much does all of this already, along with accessors generation and strict key checking.
This looks a lot like what you can do with OpenStruct, a ruby core class.
ReplyDeletehttp://www.ruby-doc.org/core/classes/OpenStruct.html
require 'ostruct'
movie = OpenStruct.new(:title => 'Godfather', :subtitles => 'English')
movie.title # => "Godfather"
movie.subtitles # => "English"
You could first base your movie class on OpenStruct and then take it from there.
class; Movie < OpenStruct; end
But, of course you knew about OpenStruct (you've blogged about it before).
So, I guess I am not clear on what the advantage is here.
-- Mike Berrow
Mike,
ReplyDeleteOpenStruct, in my opinion, is the coolest class that should never be used.
My main gripe is that any typo while using an OpenStruct will return nil instead of a NoMethodError. Those bugs are usually hard to find.
Cheers, Jay
Jay, so it the typo returning nil your main reason for rolling your own? Is there a better reason not to use OpenStruct?
ReplyDeleteBen,
ReplyDeleteYes, typos are the main reason that I do not use OpenStruct objects.
Cheers, Jay
I am new to Ruby
ReplyDeleteI experimpented with the Hash to_mod example
I wanted to ask if there is any reason not to self.class.send(:define_method ..
class Movie
def initialize(options)
options.each_pair {|k,v|self.class.send(:define_method, k, Proc.new {v})}
end
end
Thanks
James,
ReplyDeleteIf you defined methods in the way that you described they would be defined on all instances of Movie, not just the instance that was being constructed. If that's okay for what you are doing, then that implementation is fine.
Cheers, Jay
Thanks Jay!
ReplyDeleteI've been bitten by a typo when using OpenStruct and it wasn't a fun experience :)
ReplyDeleteIn this particular case we opted to this in lieu of OpenStruct:
def initialize(params)
singleton_class = (class << self; self; end)
params.each do |key, value|
singleton_class.module_eval do
define_method(key) { value } unless method_defined? key
define_method("#{key}=") { |new_value| params[key] = new_value } unless method_defined? "#{key}="
end
end
end
Thanks for turning us onto OpenStruct but it has completely bitten us in the ass. OpenStruct Crashed My Mongrel
ReplyDeleteHey Jay,
ReplyDeletewouldn't this
class OpenStruct
def method_missing(method)
raise NoMethodError
end
end
address your issue with using OpenStruct?
Cheers,
Your Italian Mate for a Glass of White Wine in Berlin
Marco,
ReplyDeleteYeah, it looks like that would work fine.
Cheers, Jay
Re: openstruct method_missing
ReplyDeleteimplementing method_missing prevents you from adding any attributes after instantiation
Bit late to comment, but if you are concerned about the crazy flexibility of OpenStruct, why not use plain "Struct"? It too is in the standard library and allows you to create an attribute-based model with little work.
ReplyDelete