{ |one, step, back| } 1 of 1 article Syndicate: full/short

Leaky Abstractions and the Criteria Library   13 Sep 03
[ print link all ]
Joel Spolsky writes about leaky abstractions. Abstractions leak when the underlying implementation shows through. A leaky abstractions that have always bothered me is moving from a small collection of objects to a database backed collection.

Here’s an example. Suppose you had a list of person objects, and you wanted to extract everybody under the age of 21. You might write some code like this.

  # ARRAY VERSION
  youngsters = people.select { |p| p.age < 21 }

This works great with small in-memeory collections. But what happens when you start storing your objects in a database. Fetching every row from the database and running the comparison on the is not only slow, it defeats the purpose of using a database. So suddenly your code becomes …

  # SQL VERSION
  youngsters = people.select "person.age < 21"

We pass in a string (instead of a block) so we can use the string to build a SQL query. Somewhere buried inside of the select method is a statement that looks something like this:

   def select(query_string)
     sql = "SELECT * FROM person WHERE #{query_string}"
     # Use the SQL string to query the database
   end

We have to switch to string encoded queries because we have a leaky abstraction.

Ryan Pavlik published an interesting Ruby library, called Criteria, that helps to plug this particular leak. Ryan’s Criteria library provides table objects that work like this …

   require 'criteria/sql'
   table = Criteria::SQLTable.new("person")
   query = table.age < 21
   puts query.select   # => "SELECT * FROM person WHERE (person.age < 21)"

Wow. Did you see what just happened? We took an ordinary Ruby expression (table.age < 21) and somehow captured it in data — data that we used to generate an SQL statment. Lets skip how this works for just a moment and consider what we can do with this.

Using Ryan’s criteria, we can now write a database backed collection that doesn’t require us to pass in SQL fragments to do arbitrary queries. Instead we can express our queries in natural Ruby syntax and let the library handle the conversion.

A collection that takes advantage of Criteria might look something like this (in part):

  class People
    def initialize(db)
      @db = db   # DBI database handle
    end

    def select(&block)
      table = Criteria::SQLTable.new("person")
      query = block.call(table)
      @db.select_all(query.select).collect { |row|
        Person.new(row['name'], row['age'])
      }
    end
  end

How it Works

The mechanism behind Criteria is surprisingly simple. It parses the expression by executing it. Sending any message to a table object will cause it to remember that message in a special criterion object, which is returned as the result of the message. Futher messages to the criterion object are also recorded and new criterion objects are returned. The end result is a network of criterion objects that resemble the parse tree for the expression being evaluated. Once you have that parse tree, the rest is easy.

Remaining Leaks

The Ruby code to generate the parse tree is about 50 lines of code, a real tribute to the flexibility of Ruby. However, it is not perfect. Since the library depends on recording messages, anything that is not a message will be lost. Since almost everything in Ruby (including operators) send messages, this is not a problem — except for the short circuit logic operators && and ||. So there’s one leak, Criteria expressions need to use & and | instead of the more natural && and || operators.

The second leak deals with how types are coerced in Ruby. The expression t.age < 21 will work because the less than message is sent to the table object and it knows how to handle it. However, the expression 21 > t.age will send the message to the integer object and it doesn’t know how to handle tables.

Fortunately the restrictions are fairly mild. The Criteria library represents some wonderful "outside the box" thinking to attack a particularly difficult problem.


blog comments powered by Disqus

 

Formatted: 14-Mar-10 11:04
Feedback: jim@weirichhouse.org