builderbuilder


Cheri builder–builder

The Cheri builder–builder is a tool for creating (or extending) builder applications, or more simply, builders. Before I go any further, let me stop and define what I mean by a builder:
A builder is a domain-specific language (DSL) for creating and/or representing hierarchies of related objects using a declarative syntax. By declarative syntax, I mean one that describes the (resultant) hierarchical structure, rather than the means by which it is created.

The Cheri builder–builder, then, is a meta-DSL for creating builder DSLs. (However, in light of the above definition, the builder–builder is not, strictly speaking, a meta-builder, or a builder at all, as its syntax is not strictly declarative.)

The builder–builder provides an easy way to create builders based on the Cheri builder framework/engine (as are Cheri::Swing, Cheri::Xml and Cheri::Html). Before I bore you with a BNF diagram of Cheri's particular take on builder syntax (honestly, I was about to), let me move on to an example, which should make things quite clear.

Example 1: A Really Simple Builder

Suppose we had a bookstore (unavoidable, it seems, in such examples), that represented a number of authors, each of whom had written a number of books. Let's build a system that captures and organizes certain information about the store, the authors, and the books. We'll keep the model simple to begin with.

class Bookstore
  attr :name, true
  attr :address, true
  attr :phone, true
  attr :authors

  def initialize(name=nil)
    @name = name
    @authors = []
  end

  def add(author)
    @authors << author
  end
end
class Author
  attr :name, true
  attr :hat_size, true
  attr :shoe_size, true
  attr :books

  def initialize(name=nil)
    @name = name
    @books = [] 
  end
  
  def add(book)
    @books << book  
  end
end
class Book
  attr :title, true
  attr :pages, true
  attr :color, true
  attr :mass, true
  attr :density, true

  def initialize(title=nil)
    @title = title
  end
end
Now that we've identified the truly relevant data, let's first look at populating our bookstore in the usual (imperative) way.
@store = Bookstore.new 'Last Writes'
@store.address = '1313 Mockingbird Lane'
@store.phone = '867-5309'

author = Author.new 'Leo Tolstoy'
author.hat_size = 6.875 *
author.shoe_size = 8.0 *
@store.add author

book = Book.new 'War and Peace'
book.pages = 1424
book.color = 'gray'
book.mass = 973 
book.density = 1.0/0.0 
author.add book

book = Book.new 'Anna Karenina'
book.pages = 1024
book.color = 'scarlet'
book.mass = 582
book.density = 9301
author.add book

author = Author.new 'Terry Pratchett'
author.hat_size 7.5
author.shoe_size 11.75
@store.add author

book = Book.new 'Mort'
book.pages = 224
book.color = 'indigo'
book.mass = 148
book.density = 0.0/0.0
author.add book

# we could go on and on.  but we won't.

* hat and shoe sizes in speculative US units
 mass in subjective units (SU)
 density in SU
Now let's look at how we'd populate the store using declarative syntax, if only we had a builder to help us out. We'll assume it's a Cheri builder, because, of course, in a moment it will be.
@store = cheri.bookstore 'Last Writes' do
  address '1313 Mockingbird Lane'
  phone '867-5309'
  author 'Leo Tolstoy' do
    hat_size  6.875
    shoe_size 8.0
    book 'War and Peace' do
      pages   1424
      color   'gray'
      mass    973
      density 1.0/0.0
    end
    # or a litte more compactly:
    book('Anna Karenina') {pages 1024; color 'scarlet'; mass 582; density 9301 }
  end
  # even more compactly
  author('Terry Pratchett') { hat_size 7.5; shoe_size 11.75
    book {title 'Mort'; pages 224; color 'indigo'; mass 148; density 0.0/0.0 }
    # we'll do a couple more for fun
    book {title 'Reaper Man'; pages 288; color 'green'; mass 218; density 42 }
    book {title 'Soul Music'; pages 389; color 'yellow'; mass 486; density -13 }
  }
end
That's a lot easier than using the imperative syntax. So, how do we create a Cheri builder to enable this declarative syntax? Like this:
# creating a really simple builder

require 'cheri/builder'

BookstoreBuilder = Cheri::Builder.new_builder Bookstore, Book, Author

include BookstoreBuilder   #=> remember Cheri builders are mixin modules
                
Of course, this example was a little contrived, in that both the Bookstore and Author classes defined an add method, which Cheri recognizes by default (for simple builders). The next example demonstrates creating a builder for classes with 'add' methods other than add.

Example 2: A Slightly Less-Simple Builder

This example is almost the same as the previous one; the only difference is that the add methods in classes Bookstore and Author have been renamed:
class Bookstore
  attr :name, true
  attr :address, true
  attr :phone, true
  attr :authors

  def initialize(name=nil)
    @name = name
    @authors = []
  end

  def add_author(author)
    @authors << author
  end
end
class Author
  attr :name, true
  attr :hat_size, true
  attr :shoe_size, true
  attr :books

  def initialize(name=nil)
    @name = name
    @books = [] 
  end
  
  def add_book(book)
    @books << book  
  end
end
class Book
  attr :title, true
  attr :pages, true
  attr :color, true
  attr :mass, true
  attr :density, true

  def initialize(title=nil)
    @title = title
  end
end
Cheri's default strategy of looking for an add method won't work here. However, we can still use the builder–builder's "really simple" form, with just a slight twist:
# creating an almost really simple builder

require 'cheri/builder'

BookstoreBuilder = Cheri::Builder.new_builder Book,
                                              Author => :add_book,
                                              Bookstore => :add_author

include BookstoreBuilder   #=> remember Cheri builders are mixin modules
                
Beyond this point, we'll have to switch from the builder–builder's "really simple", or parameter form, to its "still way easier than writing it yourself", or block form, to take advantage of additional features of the Cheri builder platform/engine.

Example 3: Connections, part 1

The "really simple" builders we created in the first two examples work fine when used as intended. However, it is also possible to use them incorrectly. For example, the following would be accepted:
@store = cheri.bookstore('Last Writes') {
  bookstore 'Inner store?'
  book 'Anonymous author?'
  author('Fictional author?') {
    author('Created by this one?') {
      author('Or this one?') {
        bookstore('Who owns a bookstore?')
}}}}
Now, for one-time use, or use only by yourself, that may be acceptable — you know you'll use the builder correctly. But for regular use, or use by others, you'll probably want something more robust. So let's look at how we can tighten things up.

As I hinted at earlier, the "really simple" (parameter) form of the builder–builder's new_builder method is shorthand for a more detailed block form. Here's what our bookstore builder looks like in block form, using the version from Example 2:

BookstoreBuilder = Cheri::Builder.new_builder do
  # specify each class to be built
  build Bookstore
  build Author
  build Book
  
  # specify how objects of each type connect to other objects
  type Bookstore do
    connect Object, :add_author
  end
  type Author do
    connect Object, :add_book
  end
  type Book do
    connect Object, :add #=> :add is the default, may be omitted  
  end
end
Each build statement specifies a class to be built. By default, the build method is the lower-case class name, with underscores inserted in place of any camel-casing; so if our bookstore class name had been BookStore, the method name would have been book_store. You can override the default build method by following the class name with the symbol for the desired name:
  build Bookstore, :store #=> override default build method 'bookstore'
The type statement defines how (and which) child objects may be connected to parent objects of the specified type (class). As you will see in later examples, the type (or types) statement is quite powerful; for example, superclasses or mixin modules may be specified in place of the actual type. (Later examples will also show connecting objects based on symbol [build method], rather than type.)

In this case, notice that the connect sub-statement for each type specifies child class Object; that is the type inserted by the "really simple" (parameter) form of the builder–builder, since it doesn't know anything about the relationships among the classes you specified, or how you intend to use the builder. Class Object will match just about any child object (we'll ignore 'orphan' classes here) — as with the type statement itself, superclasses or mixins may be specified in the connect statement.

(Also notice that the builder–builder created a type/connect pair for the Book class with the default method add, even though Book doesn't define an add method — I'll optimize this out in a later release.)

So all we really need to do to prevent illegal connections is change the connect (child) types for Bookstore and Author:

  type Bookstore do
    connect Author, :add_author
  end
  type Author do
    connect Book, :add_book
  end
What happens now if we try to build an invalid hierarchy?
@store = cheri.bookstore('Last Writes') {
  author 'Terry Pratchett'
  bookstore 'Inner bookstore?'
}
Well… as you'd expect, the author will be added to the top bookstore. But the 'Inner bookstore' just … sort of … vanishes. By default, Cheri does nothing if a child object can't be connected to a parent object. But you'll probably want to know if that happens, as it will most likely be an error of some sort. So let's issue a warning for any objects that don't get connected:
  type Object do
    connect Object do |parent, child, symbol|
      warn "warning: can't connect #{child.class} (#{symbol}) to #{parent.class}"
    end
  end
Cheri's type matching logic works from most-specific class/module (nearest ancestor) to least, so this code will only be invoked if a would-be child object can't be connected based on a more specific ancestor than Object. [Note that an upcoming version of the builder–builder will provide a shorthand method of specifying warnings/exceptions for unconnected objects.]

Here's our completed builder:

BookstoreBuilder = Cheri::Builder.new_builder do
  # specify each class to be built
  build Bookstore
  build Author
  build Book
  
  # specify how objects of each type connect to other objects
  type Bookstore do
    connect Author, :add_author
  end
  type Author do
    connect Book, :add_book
  end
  # issue warning for unconnected objects
  type Object do
    connect Object do |parent, child, symbol|
      warn "warning: can't connect #{child.class} (#{symbol}) to #{parent.class}"
    end
  end
end

So far, we've been working with a very simple model: Bookstore => Author => Book. In the next example, we'll look at creating a more complex builder.

Example 4: Connections, part 2

Coming soon! Check back a little later for the next example(s).

Copyright © 2007-2009 Bill Dortch