Ruby 101: Make your class behave like a Ruby built-in
I got re-acquianted with this scenario while working on the OpenAmplify gem – a wrapper for the OpenAmplify API. When you give the api a text like a blog comment, it will return a list of common terms, opinion scores, named locations, and other information that can be used for text mining operations.
The OpenAmplify returns key-value pairs in an XML string by default, but it can also be in JSON, CSV, or RDF format. From a Ruby client’s point of view, we want it in Hash. You can choose to use an XML library like Nokogiri but in my opinion, working with a Hash fits nicely with Ruby.
Anyway, back to the problem. I have an instance variable that holds the data. One approach is to give clients access to the instance variable.
class Response
attr_reader :data
def initialize
@data = {}
end
end
data = response.data
topics = data[‘Topics’]
One major issue with this approach is you’re exposing the internals of your class. What if you decided to rename the variable into ‘@results_in_hash_form’? Then, all programs that uses your code will break. Worse, you will be limited from enhancing the behavior of your class like lazy loading of the data. You can wrap the access to your data inside a method but that still presents the problem of exposing the internals of your class. Also, that’s an unnecessary extra line of code
My suggestion is to make ‘Response’ behave like a Hash so we can do these:
topics = response['Topics']
response.has_key?('Topics')
# And still have our own methods:
response.some_method_we_defined
So, how can we do this? The trick is to delegate the calls to the instance variable. One approach is to define the Hash methods you want to support:
class Response
[‘[]’, ‘has_key?’, ‘fetch’, ‘empty?’, ‘keys’].each do |method_name|
class_eval <<-EOS
def #{method}(*args)
@data.send("#{method_name}", *args)
end
EOS
end
end
The code above is a shortcut to writing every method by hand. If you want to support all Hash methods, that would be a lot of typing.
A better approach is to just take advantage of Ruby’s ‘method_missing’ which is called every time an undefined method is called.
class Response
def method_missing(name, *args, &block)
@data.send(name, *args, &block)
end
end
Of course, how your ‘method_missing’ will look like depends on your requirements. In our simple case, we can simply delegate to @data.
This approach is called a “Dynamic Proxy” from the book Metaprogramming Ruby by Paolo Perrota. If you want to take your Ruby skills to the next level, I highly recommend this book.