Looping in a no man’s land

…or: how to put class scope to use

The class scope, i.e. the space inside class declaration, but outside method definitions, is a no man’s land in many languages (particularly in Java). By this I mean that you can define methods, variables, and other classes there and you can even execute some code (in variable initializations) but nothing more. Fortunately, in Ruby empty space between method definitions can be filled with all kinds of useful, executable code. Let’s check a few examples.

Case study #1: Class level DSL’s

Perhaps the best example of class scope use are various class level DSL’s. All the belongs_to, has_many or validates_something directives that Rails developers use everyday are not that mysterious or strange as Java annotations. They are just class method calls that mutate our class in some way. These class methods are defined somewhere in base classes. But ActiveRecord’s DSL is not the only popular DSL in Ruby. Ruby itself uses similar directives, e.g. attr_reader etc.

In Ruby, such DSL’s are easy to create. Let’s see how easily we can create directive similar to said attr_reader, but even better. Our directive will generate reader methods and also initialize method with initialization code for the attributes. All the fancy stuff like default parameter values or error checking is left as an exercise for the reader.

class Test 
  def self.attr_with_init *args 
    args = [args].flatten.each do |a| 
      define_method a do 
        instance_variable_get "@#{a}" 
      end 
    end 
    class_eval <<-END 
      def initialize(#{args.join','}) 
        #{args.map{|a|"@#{a} = #{a}"}.join"\n"} 
      end 
    END 
  end     

  attr_with_init :foo, :bar 
end     

t = Test.new 8, 'x' 
puts t.foo 
puts t.bar 
puts t.instance_variables

This code should print:

8 
x 
@bar 
@foo

In Java, you can use annotations to achieve similar goals, but the effort you will have to put into it will probably make your code a humiliating exercise in satisfying the compiler, but far from fun.

Case study #2: similar methods

Let’s say we have a User model class with several fields that can be filled by the user. Those fields’ contents should be checked against some black list to prevent the user from using offensive words in their profile info. We want to test if the validations for all fields are in place. We can do that this way:

def test_blacklisted_content 
  %w[forename surname address about_me tags].each do |f| 
    u = User.new f => 'your_favorite_curse_here' 
    assert !u.valid? 
    assert_not_nil(u.errors.on(f)) 
  end 
end

This works but in case of failure we don’t have information about which field failed. And one failure means that all following fields are not checked by this test. We would like to have a separate test method for each field. But is this possible without copy’n’paste? Of course, we just need to move the loop outside the method. Now this may sound like an abomination for a Java-guy, but this is Ruby, remember? Designed to make programming fun!

%w[forename surname address about_me tags].each do |f| 
  define_method "test_blacklisted_content_#{f}" do 
    u = User.new f => 'your_favorite_curse_here' 
    assert !u.valid? 
    assert_not_nil(u.errors.on(f)) 
  end 
end

We only changed first 2 lines, but now we have a loop that defines five independent test methods. If three of our five fields lack proper validations, we now get three failing tests with appropriate names. The code is not much harder to understand that the previous version — any noob should grok it after reading define_method documentation.

About these ads

One response to “Looping in a no man’s land

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: