Rails Internals: named_scope Refactored

Got a question or comment about this article? Find us on twitter and let us know!.


You've probably heard that reading someone else's code is one way to learn more Ruby, and it's true. If you're a Rails developer, you already have a large quantity of reading material at your disposal: the Rails source itself. Reading the Rails source code is a great way to learn about Ruby idioms and techniques that you may not be using in your own applications.

Stop

Not familiar with named_scope? Check the API documentation or read about it in the official Rails guide.

Some Rails code is bread-and-butter Ruby, that most developers can readily understand. Other parts use more advanced idioms or a more densely-packed style, which might dissuade the casual developer from being able to understand the code. That's a shame, because valuable concepts and techniques can go unnoticed and unlearned.

We're going to take a look at the implementation of named_scope, a very popular feature of ActiveRecord that first appeared in Rails 2.1.

1. What is Refactoring?

Refactoring is an essential coding discipline for every agile developer. Refactoring is the process of taking existing code, and morphing into new code that exhibits the exact same behavior on the outside, but is cleaner, simpler, and better designed on the inside.

Martin Fowler, author of the well-known Refactoring book, identifies many kinds of refactoring patterns. One of the most common techniques - extract method - can help simplify a method that's trying to do too much, or whenever the _intent_ of the code isn't clear enough.

To demonstrate, we're going to take a look at the implementation of one of the most popular features in Rails 2.1: named_scope.

2. The 10-Second Rule

No, I'm not talking about the bagel you dropped on the floor. Take a look at the following code. It's taken directly from the Rails 2.1 source code. Can you understand it in 10 seconds or less?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Implementation of named_scope in Rails 2.1
def named_scope(name, options = {}, &block)
  name = name.to_sym
  
  scopes[name] = lambda do |parent_scope, *args|
    Scope.new(parent_scope, case options
      when Hash
        options
      when Proc
        options.call(*args)
    end, &block)
  end
  
  (class << self; self end).instance_eval do
    define_method name do |*args|
      scopes[name].call(self, *args)
    end
  end
end

Unless you're comfortable with some advanced Ruby concepts, this code may be pretty hard to understand.

There are several things going on here that make this code harder to read than it ought to be. Let's walk through them one at a time. We'll refactor as we go, leaving us with code that will behave the same, but will be make the named_scope method more readable and its intent more transparent.

The method starts innocently enough, by coercing the name parameter into a symbol.

Then we go over the cliff pretty fast. Line 5 looks like it's going to assign a key-value pair into a hash named scopes. The key for the pair is name, but the value is... well, something complicated:

5
6
7
8
9
10
11
12
scopes[name] = lambda do |parent_scope, *args|
  Scope.new(parent_scope, case options
    when Hash
      options
    when Proc
      options.call(*args)
  end, &block)
end

It's a lambda that takes multiple parameters, and which will return a new Scope object. We don't know yet what the Scope class does, but we don't need to worry about that for now. Furthermore, creating this Scope instance seems to require three parameters, although it may not be immediately obvious.

The first parameter is simply the parent_scope value that was received by the lambda. The second, however, requires a case statement to determine, and the author of this code has chosen to put the case statement inline with the rest of the code. The third parameter is the &block parameter that the named_scope method originally received.

After some digging into the Scope class, and researching where the scopes hash is used, I determined that the scopes hash maps the name of your named scope to a lambda expression that will be executed later.

3. Refactoring, Part 1

Let's refactor this code segment out to a well-named method that more clearly describes the author's original intent.

def save_scope_definition_for_later(options, &block)
  scopes[name] = lambda do |parent_scope, *args|
    Scope.new(parent_scope, case options
      when Hash
        options
      when Proc
        options.call(*args)
    end, &block)
  end
end

Now we can simplify the original code a little bit:

1
2
3
4
5
6
7
8
9
10
11
12
# New implementation after one round of refactoring
def named_scope(name, options = {}, &block)
  name = name.to_sym
  
  save_scope_definition_for_later(options, &block)
  
  (class << self; self end).instance_eval do
    define_method name do |*args|
      scopes[name].call(self, *args)
    end
  end
end

Wow, that helped a lot. Less code, more transparency. It feels a lot better already.

4. Refactoring, Part 2

We now have less code in the named_scope method. But the code that remains still feels a bit funny. Ruby methods should contain code at the same level of abstraction. The nice-looking call to save away the scope definition is followed several lines of very detailed-looking code.

Let's take a closer look at the remaining code:

(class << self; self end).instance_eval do
  define_method name do |*args|
    scopes[name].call(self, *args)
  end
end

There are actually two advanced Ruby idioms in use here. Suffice to say, this code will generate a new method in your model class. The name of this new method is made to be the same as your named scope; and the implementation is actually quite simple: it first finds the appropriate scope definition that had previously been saved away, and executes it.

Let's again factor this out to a well-named method:

def create_method_with_same_name_as_scope(name)
  (class << self; self end).instance_eval do
    define_method name do |*args|
      scopes[name].call(self, *args)
    end
  end
end

And now, the named_scope method has been reduced to this:

1
2
3
4
5
6
# Clearer code
def named_scope(name, options = {}, &block)
  name = name.to_sym 
  save_scope_definition_for_later(options, &block)
  create_method_with_same_name_as_scope(name)
end

We can even do away with that reassignment of the name variable as well, and perform the conversion on the fly:

1
2
3
4
5
# Refactored version
def named_scope(name, options = {}, &block)
  save_scope_definition_for_later(options, &block)
  create_method_with_same_name_as_scope(name.to_sym)
end

5. What Did We Learn?

Imagine if our final version of the named_scope was the _first_ version you saw. You'd probably have a much easier time understanding how it works because there are two methods, each descriptively named, that provide insight into their intent.

Refactoring is great way to learn code that you don't understand. Begin slowly, and work to understand what the code does. As you gain understanding, you may find that extracting out the more complicated segments into well-named methods will bring a level of clarity that you did not have when you started.

Note

For a complete introduction to the practice of refactoring, we recommend Martin Fowler's seminal work, Refactoring: Improving the Design of Existing Code.