Eliminating code duplication with metaprogramming

Duplication of code is one of the worst code smells. It should be refactored in order to keep DRY if only possible. This is generally easy. But what if the code itself is not duplicated, but its structure is? That could be a little bit more difficult, but with Ruby‘s metaprogamming facilities it’s not that hard. Read on to see how.

The problem

In our project, we have a module called Factory that produces object needed by tests. It generally contains pairs of methods, new_something and create_something, like this:

module Factory
  def self.new_user options={}
    User.new({
      # some default values
    }.merge(options))
  end

  def self.create_user options
    u = new_user options
    u.save!
    u
  end
      
  def self.new_role options={}
    Role.new({
      # some default values
    }.merge(options))
  end

  def self.create_role options
    r = new_role options
    r.save!
    r
  end
      
  def self.new_permission options={}
    Permission.new({
      # some default values
    }.merge(options))
  end

  def self.create_permission options
    p = new_permission options
    p.save!
    p
  end

  def self.new_group options={}
    Group.new({
      # some default values
    }.merge(options))
  end

  def self.create_group options
    g = new_group options
    g.save!
    g
  end
  # ...snip...
end

Basically, the new_something methods merge provided and default options and return new, unsaved objects. In place of “some default values” there is always a list of attribute values that are needed for an object to pass validation (like login and password for user). This list is of course different for each type.

Notice the pattern with create_something methods?1 Yes, they always do the same thing: call the paired new_something method, save the object and return it (although the type of the object is different). How to refactor this pattern and remove the duplication? There are several ways to do it. Let’s look at one of them.

The solution

My proposed solution is not to write the create_something methods at all. Instead, provide a method_missing implementation that will take care of creating objects. Like this:

module Factory
  def self.method_missing name, *args
    if name.to_s =~ /^create_(.+)/
      obj = self.send("new_#{$1}", *args)
      obj.save!
      obj
    end
  end

  # ...snip...
end

Now, if we intercept a call to create_something, we extract the ‘something’ with a regular expression and call the appropriate new_something method, passing all the arguments just as the original methods did. Then the object is saved and returned, as before. This part is 3 lines of code, but we can make it one line:

module Factory
  def self.method_missing name, *args
    if name.to_s =~ /^create_(.+)/
      returning(self.send("new_#{$1}", *args)) { |o| o.save! }
    end
  end

  # ...snip...
end

Or, if we (ab)use the Symbol#to_proc idiom (and forsake a little bit of readability), we can shorten it further to:

module Factory
  def self.method_missing name, *args
    if name.to_s =~ /^create_(.+)/
      returning self.send("new_#{$1}", *args), &:save!
    end
  end

  # ...snip...
end

What if one of the create_something methods differs from the pattern? E.g. create_user method might need to assign the created user to a role in order to let it login into the application. That’s no problem at all. Just write your create_user method as before and let it do all you need it to do. The method_missing method will not be called when create_user is not missing. The general rule is: method_missing handles the typical cases and create_something methods are necessary only when something extra is needed.


1 There is also a pattern in the new_something methods, of course, but eliminating it is left as an exercise for the reader (and you still have to provide the default values somewhere).

About these ads

6 responses to “Eliminating code duplication with metaprogramming

  • Alex G

    ya… because that makes the code that much more readable :)

    i recommend not going overboard with dry. this kind of thing can back fire when you trying to figure out what the hell is going on 6 months later.

  • szeryf

    I agree, you shouldn’t go overboard, but I don’t think the first version of method_missing is that unreadable. And I would certainly prefer _one_ method_missing that _twenty_ create_somethings :) Especially if I wanted to change something in these methods (like changing save! to save or displaying some info when the object is not valid).

  • This Week in Ruby (June 26, 2008) | Zen and the Art of Programming

    […] collect, inject and detect, Enumerating Enumerable, Macros, Hygiene, and Call By Name in Ruby Eliminating code duplication with Metaprogramming. Also noteworthy, this piece on working with Microformats from […]

  • dave

    I’m just trying to learn Ruby (and all this metaprogramming stuff at the same time) but shouldn’t there just be ‘new_with_defaults’ and ‘create’ methods on the classes themselves? I don’t really see what the factory is adding here. Isn’t this kind of code sharing between classes what Mixin’s are for.

    (apologies if I’m talking nonsense, just trying to learn)

  • szeryf

    Dave, you’re not talking nonsense :) I just forgot to mention that this Factory class is used in a Rails project and it’s dealing with Rails’ model classes.

    These classes have (among others) two common methods, new and create, that accept a hash with attributes and return new objects. The create method also tries to save it. Our Factory methods mimic this behavior for the tests.

    The defaults are mostly useful for the tests only — normally you don’t create users with logins like "test#{rand(1000)}", do you? :) That’s what the Factory is adding: those defaults are gathered in one place, rather than being sprinkled across many classes. Having them in one place improves refactoring possibilites and code integrity (i.e. even if they are copy-pasted, it’s still easier to make them look similar and work as expected).

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: