agile-ajax

Using the Null Object Pattern With ActiveRecord

buy_an_o.jpg

I'm working on a project estimating and tracking tool that we use internally here at Pathfinder. In that tool, each task is normally assigned to a specific user, however it's not at all unusual for a task to live in the database for some time in an unassigned state. This leads to a lot of special case code along the lines of:

if task.user? then user.full_name else nil

This is a little bit annoying to continuously write, so I originally added a fake user into the database named "Unassigned", and made that the default user for a new task. This works, and cleans up the code a bit, but it does seem a little hackish to have code actually depend on the existence of a specific database record. Also, as the data model got more complex, I wound up having to test for the Unassigned user later in the code, which is also becoming bothersome.

I decided to refactor the code using a variant of the Null Object pattern, which creates a specific class in code to represent the unassigned user, rather than an arbitrary database row. Unlike most of the standard Design Patterns, the Null Object pattern adapts quite usefully to a Ruby application.

Start with a test. The test verifies that a new task gets an instance of the UnassignedUser class. After verifying the type of object, you're free to test any other aspects of the Null Object -- in my specific case, those features are covered in other tests.

The Task class referenced here has a belongs_to: user relationship, which is the attribute that will need to return the Null Object.

def test_unassigned_user
actual = Task.new
assert_equal(UnassignedUser, actual.user.class)
# test for specific features of Unassigned User here
end

In order for this to work, we need to hijack the user attribute method in the Task class to return the Null Object if there is no assigned user. To do that correctly, we need to effectively surround the original functionality with our new guard clause, but we don't want to have to rewrite the ActiveRecord mechanism for retrieving normal users. If Rails was designed slightly differently, we could easily use super here, but since the superclass of Task is ActiveRecord::Base, which doesn't have user defined, it's easier to surround the functionality using Rails' alias_method_chain feature:

def user_with_null_object
return User.unassigned_user if user_id.nil? or user_id == 0
user_without_null_object
end

alias_method_chain :user, :null_object

In Ruby, a method name can be directly assigned to another method using the alias keyword. The alias_method_chain method is a Rails addition that allows you to create a new method that wraps the existing method by creating two separate method aliases.

The first argument (in this case :user) is the existing method to be aliased, and the second argument (:null_object) is a symbol uniquely identifying the alias chain. The aliases allow the existing method (user) to redirect to <METHOD>_with_<SYMBOL>, while the old functionality can be referenced using the alias <METHOD>_without_<SYMBOL>. In this specific case, when the rest of the application calls Task#user, the new method user_with_null_object is invoked. The special unassigned user object is returned if the user_id is empty. Otherwise, the original ActiveRecord mechanism is called via the alias user_without_null_object.


Shameless promotion: There's more about alias_method_chain and a whole lot of other Ruby and Rails metaprogramming goodness in my new book Professional Ruby on Rails. Now back to the show.


All the unassigned_user method needs to do is return an instance of the null object class. In this case, I store a single instance as a class variable, but that's a pretty minor space optimization, you could just return a new instance every time.

def self.unassigned_user
@@unassigned ||= UnassignedUser.new
end

The UnassignedUser class itself is very simple, here's what I started with:

class UnassignedUser < User

def real_user?
false
end

def save
false
end

def update
false
end

def firstname
"Unassigned"
end

def lastname
"User"
end

end

UnassignedUser has to be a subclass of User. Frankly, this wouldn't have been my first choice, I would have rather had them just be duck-type similar, but Rails won't let an Unassigned user instance be returned as the user attribute unless it's a subclass, you get a type mismatch error otherwise.

Since it is a subclass of User and therefore ActiveRecord::Base, I want to make sure that it won't be accidentally saved to the database, so I overwrote save and update to just return false. I actually think I only need to overwrite save... The firstname and lastname attributes are hardwired, and allow the unassigned user to appear in lists, such as the lists in the application that show which user has been assigned a particular task.

At this point, somewhat amazingly, all my tests pass. The only pre-existing tests that broke were tests that specifically referenced the unassigned record in the test database. A couple of minor UI tweaks to ensure that the unassigned user is available on lists and displays where needed, and everything seems good to go.

I like how this Null Object turned out -- it removes an ugly dependency on data with a very small amount of additional code. Now, I'll have to see how this plays out in terms of the running and further development of this tool...

Topics:

Comments: 2 so far

  1. Hi,

    Wouldn’t it be better to have a record with the Unassigned / Anonymous user? It is a dependence but you can create the record from your application (for example, on startup create_if_not_exists the anonymous user).

    The advantage is that you can give AnonymousUser a closer behavior to the normal User. If you have associations with User, you could have to mimic them in Anonymous. Say your User has some roles/memberships/privileges, then it is easier to enforce that your Anonymous has some default role/membership/privilege and to modify/add/delete some in the future.

    Comment by Kenny Debeck, Tuesday, August 12, 2008 @ 3:33 pm

  2. Thanks for this clear explanation Noel.

    I was wondering how this would look to clients who were still interested in whether the user was real or not.

    Obviously using this pattern should clear up 90% of these cases, but there might still be a few.

    As I understand it, it’s impossible in ruby to make a real class look like nil (probably a good thing), so you’d have to do this:

    if task.user != user.unassigned_user
    # do something special with this assigned task
    end

    I think that’s OK. It’s a bit more verbose than just calling task.user? but also very clear about what’s going on. I also like the fact that we’ve introduced the first-class concept of an ‘unassigned user’ into the domain.

    Comment by Matt Wynne, Tuesday, October 7, 2008 @ 12:55 pm

Leave a comment

Powered by WP Hashcash

Who is Pathfinder?

Topics

Search

WordPress

Comments about this site: info@pathf.com