Saturday, December 23, 2006

Contextual Decoration of a Ruby object

One of my current work problems is that I have an Account class that needs optional decoration if it has been initialized in the context of an Import. I was kicking around an Observer or AspectR but the latter library wreaks of Java, and the former just isn't as seamless as I'd like. Also, I need to be mindful of multiple threads. Below is the solution I came up with, without the problem specific code.

The situation is actually more complicated than what I am modeling below.


class Import
attr_reader :start_at, :stop_at
class << self
def run!(&block)
Thread.new do
Thread.current['import_log'] = import_log = self.new
import_log.start!
yield block if block
import_log.stop!
end
end
end
[:start, :stop].eacha do |m|
define_method("#{m}!") do
instance_variable_set("@#{m}_at", Time.now)
end
end

def log_change(message)
... magical stuff happens here
end
end

class Account < ActiveRecord::Base
attr_reader :import_log
def initialize
@import_log = Thread.current['import_log']
super
end

def after_save
import_log.log_change("Saved Account with ID=#{self.id}") if import_log
end
end


What is actually happening is that I have numerous objects that use the "is_a_import_model" interface. If a class "is_a_import_model" then its initialize method needs to be overwritten to account for the import_log. There is also a StreamParser that is reading an XML document to process the lots of imports. If the StreamParser is created in the context of an Import.run!, it needs to log and rescue all exceptions, otherwise, it re-raises those exceptions.

Tuesday, December 19, 2006

Sandman and Desire

This week, I received an Amazon gift certificate. I spent quite awhile deliberating and thinking about what I wanted. Should I get Zelda for the Wii? Or should I get a couple of game books that I want? What about my wishlist?

I looked on my wishlist and started investigating what Dungeons and Dragons books I wanted. I looked at reviews and was close to ordering when I decided to give my wishlist a final review. There were a few books that I think would be truly interesting (Prime Obsession by John Derbyshire, The Master of Go by Yasunari Kawabata, A History of the Ancient Near East by Marc Van de Mieroop, etc). There were a couple of D&D books that I had earmarked, as well as a couple of CDs. And then there are the Sandman volumes (by Neil Gaiman), the perpetual inhabitant of my wishlist.

Four years ago, I borrowed the 10 volume Sandman series from a friend. I started reading it, and was immediately drawn in by Neil Gaiman's prose. I was hungry, and devoured the volumes in rapid succession, feeling a bit of remorse that I wasn't savoring this feast. As the series drew to a close, I knew that I wanted to own a copy of this particular work of art. The story so seamlessly dovetails with the human mythos that I can't help but feel like it is a part of me.

What is even more odd, is that during college, the Sandman series was being released, and several of my comic-book geek friends were all eagerly awaiting the next issue. They would talk about it, discuss and dissect and absorb each issue. I sort of wonder how I missed out on this phenomenon (ah yes Magic the Gathering and D&D). In any case, having read the 10 volume series I knew that I wanted to own it so I could read and re-read it.

As the years have passed, I've always thought about getting the whole shebang, but could never quite rationalize spending $150 in one pop. I also never felt quite right about just getting one book at a time (there is a part of me that figures that I'll stop at 6 books or something silly).

So with the above in mind I decided yet again to forgo getting the Sandman series, but I noticed something strange. Volumes 1 & 2 were not on the list. Had I missed them? No one ever orders anything from my wishlist, so I went through the steps of adding them back on, but lo, they refused to be added to the wishlist. Someone had in fact gotten me two of them.

So I cleared my shopping cart of some D&D books, and added the remaining 8 books. The gift certificate won't cover it all, but thats a small price to pay for literary genius.

Thursday, December 14, 2006

Lightsky Rocks!!!

Today was our Lightsky Christmas Break. We had snacks, drinks, and were given our Christmas present. Each employee received a Wii (actually 6 Wii were given out and there were three IOUs). The gift was most certainly a surprise. But what really impressed me was the amount of time and effort that went into these gifts. There were several early mornings, frequent visits to stores over lunch or on the way home, conscription of spouses to help get them. In all, a real dedication and desire to make sure that everyone got one. So while my place of employment rocks, its really my co-workers and the owners that rock.

I am stunned by this gracious gift of time and money.

Tuesday, December 12, 2006

Protected Find

One of the greatest professional compliments I've ever received was regarding a system that I designed and developed. The purpose was to provide an interface for numerous reporting systems to funnel reporting data into a unified location. I designed the system so that all data was retrieved via task specific finders (i.e. :find_agreement_number, :find_current_document_type). As a result, if the database changed, the methods could still be used (albeit with a tweak to the internal logic). Since I was working in a compiled environment where changing a table meant recompiling the functions that directly accessed the table, the need for public finders was required.

The compliment I received was that 6 months after the design was done, the vendor's system that we were integrating with required a major upgrade and changes. And in order to make my system work, they just pointed the finders to a different table, and presto things were working again.

Now what does this have to do with Ruby on Rails? Today I was having a conversation with a co-worker regarding find. After some discussion, I came out and said I really wish :find were a protected method. Find is a very powerful method that allows you to fully access all data from anywhere. This power exposes your your view, controller, and other models to the risk that your model will change. To reduce this risk, I feel it is important to create specific finders in your model and test them. Also, if you use :find_by_name (a free method on any table that has a :name column), you should also write a test for this (at a minimum making sure that your model responds to that particular find).

The entire goal of creating specific finders is to make the maintenance of a project easier. In addition, it is much easier, in Rails, to test models than controllers or views.

Saturday, December 09, 2006

Pimping a Controller

I've recently started working on a REST based rails project. I've been thinking about the Rails model associations, and in particular has_many :through, which is used to help expose the join between two models. Given the following three models:



class User < ActiveRecord::Base
has_many :permissions
has_many :roles, :through => :permissions
end
class Permission < ActiveRecord::Base
belongs_to :role
belongs_to :user
end
class Role < ActiveRecord::Base
has_many :permissions
has_many :users, :through => :permissions
end



I want to be able to look at permissions in a few different ways (and roles and users for that matter). Now, I can modify my PermissionsController to set the object that will be used for all find calls. By default, the PermissionsController will use Permission.find, but if I swap the Permission object for @finder_object, I can set that @finder_object before_filter. See below:



class PermissionsController < ActiveRecord::Base
before_filter :determine_finder_object

def index
@permissions = @finder_object.find(:all)
# respond to stuff, though you might want to use different templates
# based on the finder object
end

def determine_finder_object
if params[:user_id]
@finder_object = User.find(params[:user_id]).permissions
elsif params[:role_id]
@finder_object = Role.find(params[:role_id]).permissions
else
@finder_object = Permission
end
end
end



Now, is this the best practice? I don't know, however, it feels like its something to explore. One noticeable caveat is that if you are using a REST-based controller, you will want to look at all of your Object.new calls and think about replacing them with @finder_object.build.

As an aside, the above controller would probably end up with several chunks of code that were:


if params[:user_id]
#do something for users
elsif params[:role_id]
#do something for roles
else
#do something in the generic
end


What can I do to encapsulate this particular logic? I dug up the code from the :respond_to controller instance method, and created a construct for:



determine do |d|
d.user_id { #do something for users}
d.role_id { #do something for roles}
end



I'm not satisfied with it, but am wondering what I might be able to do.