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