Saturday, October 20, 2007

Ruby: Defining Class Methods

There are several ways to define class methods in Ruby.

class Person
def Person.find(id)
...
end
end

This is the version that recent Java/C# converts seem to prefer. I'm not a big fan of this version because it requires me to change all the class method definitions if I change the name of the class.

class Person
def self.find(id)
...
end
end

I prefer this version. The syntax is concise and descriptive. When browsing a file of code, the use of self. makes it very clear that the method is a class method. This is the version I use by default.

class Person
class << self
protected
def find(id)
...
end
end
end

This is the version I use when I need to make the method protected. For more information on why this is necessary you can check my previous entry: Protected Class Methods

class Object # http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html
def meta_def name, &blk
(class << self; self; end).instance_eval { define_method name, &blk }
end
end

class Service
def self.responses(hash)
hash.each do |method_name, result|
meta_def method_name do
result
end
end
end

responses :success => 20, :unreachable => 23
end

Service.success # => 20
Service.unreachable # => 23

This version is by far the most complicated to write and read. The justification for using this version is that you can create class methods that declaratively define other class methods. This version requires the use of define_method instead of def because variables from the surrounding context are necessary. I tend to only use this version when I'm metaprogrammatically defining other class methods.

class Person
instance_eval do
def find(id)
...
end
end
end

It's also possible to define class methods inside an instance_eval sent to a class. For an explaination of why this works check out instance_eval and class_eval method definitions. As the linked entry states, I have run into this by accident, but I don't think I've ever actually used instance_eval to define class methods on purpose.

Are there ways that you define class methods that I've left off. If so, please let me know how and why in the comments (with an explanation inline or a link to a blog post on your site).

10 comments:

  1. Anonymous2:15 PM

    It looks mightily confusing when the syntax highlighting has hidden the underscores in "meta_def" and "instance_eval". For a minute I thought there was some trick I wasn't aware of, but the html source revealed that there indeed are underscores in both.

    ReplyDelete
  2. Hi Jay,

    One other way could be putting your methods in a module and then in your class : extend ClassMethods

    If you're looking for a better way for your readers to submit code may I suggest cross-posting to http://refactormycode.com/ and using the trackback feature.

    ReplyDelete
  3. Anonymous9:26 AM

    Two more class_eval examples are http://snippets.dzone.com/posts/show/3378 and http://snippets.dzone.com/posts/show/2786

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. I prefer the singleton class way. Something about seeing 'def self.' over and over gets to me, but then again I try to use self as a receiver only when necessary.

    It also lets you do other things more naturally, like declaring some methods private (not protected, since that doesn't make sense for a class method), and creating aliases.

    ReplyDelete
  6. Hi Jay, I somehow seem to remember that the Person.class_meth form used to bomb when invoked via a subclass (sample code below). I tried it on Ruby 1.8.6 and it doesn't bomb anymore. Did it ever bomb in a previous Ruby version or am I day-dreaming??

    Eg:
    class Person

    def Person.my_class_meth
    "hi"
    end

    def self.another_class_meth
    "bye"
    end

    end

    class Employee < Person
    end

    Employee.my_class_meth # Did this ever bomb with undefined method?!

    Employee.another_class_meth # This always worked

    ReplyDelete
  7. As far as I know it always worked, but since I don't use that form I'm not the authority on it.

    On my first project we used Ruby 1.8.4 and it seemed to work fine...

    ReplyDelete
  8. Graeme Defty12:29 AM

    Another drawback of the first form which the others avoid is that the class name occurs multiple times in the source for the class definition. If it needs to be changed for any reason (e.g. you realise it was a bad choice, or you want to clone the class as a start-point for a similar one) it has to be changed in multiple places. Not a huge effort, but irksome.

    ReplyDelete
  9. Very helpful information, thank you.

    ReplyDelete
  10. Anonymous8:17 AM

    "This [, Class.method,] is the version that recent Java/C# converts seem to prefer. I'm not a big fan of this version because it requires me to change all the class method definitions if I change the name of the class."

    I think it's all a matter of perspective. Class.method seems less magical and makes more sense to me. Especially since Self changes so readily.

    Since many Ruby purists frown on this I avoid doing it but I dislike Self.method as it is less clear to me. ESPECIALLY when refactoring; contrary to your complaint. If I were to use Class.method I would instantly know what class I'm refactoring and wouldn't have to worry about some recursive reference to Self that might or might not be in play. Using the class name is more precise while using self muddies the waters.

    Also when you say, "...it requires me to change all the class method definitions if I change the name of the class." Is just plain silly and points to the weaknesses of using Textmate and other such editors instead of full-fledged IDE's. You're now in a mindset that refactoring is a chore when it should be the focus of good Behaviour-Driven Development. All you have to do is select your class, press the find/replace key combo, and swap out your class name with the new name. Easy.

    That being said I actually do use Textmate for Ruby and Rails development simply because there currently are no good IDE's.

    ReplyDelete

Note: Only a member of this blog may post a comment.