Sunday, February 24, 2008

Ruby: Creating Anonymous Classes

Classes in Ruby are first-class objects—each is an instance of class Class. -- ruby-doc.org
Most classes are defined using the syntax of the following example.

class Person
...
end

Defining classes this way is familiar to most developers. Ruby provides another syntax for defining classes using Class.new. The following example also defines the Person class.

Person = Class.new do
...
end
Class.new(super_class=Object) creates a new anonymous (unnamed) class with the given superclass (or Object if no parameter is given). You can give a class a name by assigning the class object to a constant. -- ruby-doc.org
The following example shows that the new class begins as an anonymous Class instance, but becomes a named class when it's assigned to a constant.

klass = Class.new
klass.ancestors # => [#<Class:0x21b38>, Object, Kernel]
klass.new.class # => #<Class:0x21b38>

Person = klass
Person.ancestors # => [Person, Object, Kernel]
Person.new.class # => Person

klass.ancestors # => [Person, Object, Kernel]
klass.new.class # => Person
klass # => Person

I generally use traditional syntax when defining named classes, but I have found anonymous classes very helpful when creating maintainable tests. For example, I often need to test instance methods of a Module. The following tests show how you can define a class, include a module, and assert the expected value all within one test method.

require 'test/unit'

module Gateway
def post(arg)
# implementation...
true
end
end

class GatewayTest < Test::Unit::TestCase
def test_post_returns_true
klass = Class.new do
include Gateway
end
assert_equal true, klass.new.post(1)
end
end

There are other ways to solve this issue, but I prefer this solution because it keeps everything necessary for the test within the test itself.

Another advantage to defining classes using Class.new is that you can pass a block to the new method; therefore, the block could access variables defined within the same scope. In practice, I've never needed this feature, but it's worth noting.

6 comments:

  1. Do you like extend?

    class GatewayTest < Test::Unit::TestCase
    def test_post_returns_true
    obj = Object.new.extend Gateway
    assert_equal true, obj.post(1)
    end
    end

    ReplyDelete
  2. Hi Rubikitch,

    I love extend. For this example it would work. This is the problem with example code, you have to be concise enough that you make the point, but not so concise that it appears a simpler solution will do. When I'm not using a contrived example I usually need to include a module and do something else, such as call a class method.

    For exmaple,

    klass = Class.new do
      attr_accessor :name
      include Validatable
      validate_presence_of :name
    end

    But, I actually like when people point out that for simple cases there are simpler solutions. It helps keep me from over-engineering.

    Thanks for the example, Cheers, Jay

    ReplyDelete
  3. irb(main):001:0> foo = Class.new
    => #<Class:0xb7cf9650>
    irb(main):002:0> foo.new
    => #<#<Class:0xb7cf9650>:0xb7cf3138>
    irb(main):003:0> Dink = foo
    => Dink
    irb(main):004:0> foo.new
    => #<Dink:0xb7ce99a8>
    irb(main):005:0> Donk = foo
    => Dink
    irb(main):006:0> Donk.new
    => #<Dink:0xb7ce2cd4>


    Weird .. I wonder where the mapping between class instance and constant name is kept?

    ReplyDelete
  4. The major problem with Class.new is can't specify a class name so you have no way of referencing it later.

    You might say, "Well who cares, you've got a variable that references the class." The issue is I need the class name because I'm doing dynamic class generation testing. I need to store the class name so I can test an actual class#method invocation as it will be used in the wild. Not being able to specify the class name is very annoying.

    ReplyDelete
  5. You could dynamically set the variable to a constant to set a name:

    ruby-1.8.7-p352 :001 > module Bar; end
    ruby-1.8.7-p352 :002 > t = Class.new {}
    ruby-1.8.7-p352 :003 > Bar.const_set('Jabberwocky', t)
    ruby-1.8.7-p352 :004 > t.name
    => "Bar::Jabberwocky"

    ReplyDelete
  6. Anonymous10:43 AM

    You could dynamically set the variable to a constant to set a name:

    ruby-1.8.7-p352 :001 > module Bar; end
    ruby-1.8.7-p352 :002 > t = Class.new {}
    ruby-1.8.7-p352 :003 > Bar.const_set('Jabberwocky', t)
    ruby-1.8.7-p352 :004 > t.name
    => "Bar::Jabberwocky"

    ReplyDelete

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