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).