A_Ruby_on_Rails_Builder_Implementation

Quick Profile

Alex Ooi Profile Picture
Hometown: Melbourne, Australia
Specializations: Java, Ruby on Rails
University: Software Engineering & Economics, Melbourne University
High School: VCE, Melbourne High School
Links:
 

Popular Articles

Saturday, July 05, 2008

I'm sitting in Sydney airport waiting for my flight back to Melbourne, so I thought I'd complete this little blog article that I've been meaning to write ever since I implemented a similar solution in a recent Ruby on Rails project.

Anyway, let us proceed ...

Creating test data can often require the construction of complex data object, usually with many dependencies that need to be created at the same time as the data object. The Builder pattern greatly simplifies the code required to create such objects, and the implementation of this pattern is especially easy in a dynamic language such as Ruby. The result is a highly customizable builder that allows your tests to create very specific and complex data structures with the minimum of code.

Note: What will be covered is a very simple Builder, simply covering the basics of the implementation to demonstrate the potential of the Builder pattern in Ruby. Different projects have different needs, and it is quite possible (and highly probable!) that a Builder in a real system will have more features built into it.

The Common Problem

A common domain model for a system revolves around the concept of Products. Whilst a Product often has many direct attributes, such as name, description and price, it will often have many associations of either the one-to-many or many-to-many variety. Such examples include Brands, Retail Chains, Categories, etc. Using RoR's ActiveRecord pattern to save Products often involve verbose code such as:

Product.create!(:name => "Yellow Submarine", :description => "Often found in the navy", 
                :price => 99.99, :brand => Brand.new(:name => "ADF"), 
                :categories => [Category.new(:name => "Ship"), 
                                Category.new(:name => "Submarine")] 

Imagine using that syntax whenever you need to create a Product. It is simply too tedious and verbose to contemplate. The remainder of this blog will use this scenario to demonstrate an effective solution.

What we want to achieve

The final solution must present us with the following features:

  1. It should be concise
  2. It should allow us to provide values for any attribute of a Product that are relevant to a test case
  3. It should provide sensible defaults for any attributes of a Product that are irrelevant to a test case

A Builder Pattern in the tradition of Ruby

Ruby's method_missing presents us with the ideal tool to implement an effective, scalable and concise Builder. A new class, Product Builder, will be required for the building of Products. Note that a Builder class is not required for all of your application's domain models; just the ones that are the root of a tree of complex associations (such as a Product, but not necessarily a Brand or Category).

The Boiler Plate Code

Every story has to start somewhere, so let us start by defining a Product Builder class with an intializer that creates a Product internally with a set of sensible defaults:

class ProductBuilder

  def initialize
    default_attributes = {
      :name => "Yellow Submarine",
      :description => "A new addition to our toy store",
      :price => 99.99,
      :brand => Brand.new(:name => "A Brand"),
      :categories => [Category.new(:name => "A Category")]
    }
    @product = Product.new(default_attributes)
  end

...

The Product Builder now contains an instance of a Product with default attributes. At this point the builder is able to return us a fully constructed Product provided we implement the following methods (one to return the Product in a persisted state, and the other to return the Product in a transient state):

...

  def create_without_save
    return @product
  end

  def create!
    @product.save!
    return @product
  end
...

The create! method is useful if you are creating an integration test and don't want to manually save a Product each time. The create_without_save method is useful when writing unit/mock tests, where a database is not required (and would otherwise unnecessarily slow the test down). There are, of course, other scenarios where each of these methods will be useful.

The Magic of Ruby's method_missing

Everyone has seen implementations of the Builder Pattern in various forms, one of the most ubiquitous being Java's StringBuilder/StringBuffer. However, whilst static languages such as Java must define each builder method at compile time, Ruby's method_missing allows us to implement a Builder pattern without defining a single builder method explicitly. Following is the method_missing implementation for the Product Builder:


...
  def method_missing(method_name, *args, &block)

    method_name = method_name.to_s.gsub("with_", "")
    setter_method_name = method_name+ "="

    if @product.respond_to?(setter_method_name)
      @product.send(setter_method_name, *args)
    end

    return self
  end
...

The method_missing is actually very simple. It extracts out the attribute name to set from the method name, meaning that a call to a method such as 'with_name("Blue Bike")' will be interpreted as setting the Product's 'name' to "Blue Bike".

A simple check as to whether the Product will accept the attribute is made and, if the product does support this attribute then the value is set in the Product.

Finally, note the ubiquitous 'return self' call at the end, allowing callers to build up multiple calls to the Product Builder on the same line.

The Product Builder in Action

So, what does the Product Builder look like in action? Let us assume that we are writing a test case where we want a set of Products with varying prices and brands, but we don't care about any other attribute. Here is how we may create 2 such products:

  ProductBuilder.new.with_price(45.99).with_brand(Brand.new(:name -> "Sun")).create!
  ProductBuilder.new.with_price(50.01).with_brand(Brand.new(:name -> "IBM")).create!

If we cared only about the name and didn't want to have the products persisted:

  ProductBuilder.new.with_name("Bobs Car").create_without_save
  ProductBuilder.new.with_name("Jims Bike").create_without_save

Notice how the same Product Builder allows us to quickly and concisely create Product's with the attributes that we care about, whilst sensibly defaulting the attributes that we don't care about.

Summary

The Builder pattern is an especially effective tool in a developers repertoire when building complex data structures. A dynamic language such as Ruby makes the implementation of a Builder pattern even easier and more concise. The Builder pattern is especially effective when creating test data with large association trees, enable the test case to create very specific test data with a lot of flexibility and with minimal code.

It's an extremely effective pattern for creating test data, especially when coupled with avoiding the use of RoR's Test Fixtures (something I've covered in a previous blog).

Comments ...

like your new blog's quote.
Posted by quan on Thursday, July 17, 2008 at 10:31 AM
I was very pleased to find this site.I wanted to thank you for this great
read!! I definitely enjoying every little bit of it and I have you bookmarked
to check out new stuff you post.
Posted by louboutin on Saturday, May 22, 2010 at 11:11 AM
Awesome article as usual, thanks for posting such helpful content on a regular
basis.
Posted by ??? on Sunday, May 23, 2010 at 01:19 AM
Resources like the one you mentioned here will be very useful to me! I will
post a link to this page on my blog. I am sure my visitors will find that very
useful.
Posted by mbt shoes on Tuesday, May 25, 2010 at 06:38 PM
Good read. There is currently quite a lot of information around this subject on
the net and some are most defintely better than others. You have caught the
detail here just right which makes for a refreshing change – thanks.
Posted by sf on Sunday, June 20, 2010 at 07:23 PM

Add a Comment

*
*
You must answer the following simple maths question before your comment will be accepted.
*