[UPDATE] I made significant updates to Siffer because of this post and as a result the code you see here is no longer up-to-date. http://github.com/clinth3o/siffer and look at the Siffer::Xml.
A part of the SIF that I am implementing in Siffer is the Data Models. This part of Siffer could be extremely tedious. There are 101 of these things.
Each data model has it’s own elements (values if you will), which some are repeatable, conditional or mandatory. I also have to be able to output them in XML format. The end goal would be to output something like this for an Address Data Model:
<Address Type="0123">
<Street>
<Line1>1 IBM Plaza</Line1>
<Line2>Suite 2000</Line2>
<StreetNumber>1</StreetNumber>
<StreetName>IBM</StreetName>
<StreetType>Plaza</StreetType>
<ApartmentType>Suite</ApartmentType>
<ApartmentNumber>2000</ApartmentNumber>
</Street>
<City>Chicago</City>
<County>Cook</County>
<StateProvince>IL</StateProvince>
<Country>US</Country>
<PostalCode>60611</PostalCode>
<GridLocation>
<Latitude>41.850000</Latitude>
<Longitude>-87.650000</Longitude>
</GridLocation>
</Address>
I don’t want to write a class for each Data Model (remember there are 101 of them). That would suck. But it’s a required portion of the framework and I have to. So how do I do them in a way that is easy and fun?
I call it Construction Driven Design. I’m not sure I am the first to really come up with this concept. I did some googling and couldn’t find anything so I thought I’d share my idea. So here is the best definition I can come up with:
Defining the template of a class and how you want to write the class prior to any implementation.
Which is to say: Mock up the way you want to code each class, then fill in the details to make it work. The template portion of the definition is the key. First you create how you will “template” each of these similar classes before the “base” class ever exists. The most important part however is the how. You are defining how you want to write all of these classes. And that is where the term “Construction” comes into play. The design of the code is driven by how you want to construct the code physically. The Data Model section of the SIF is really the best example of when to use CDD. The circumstance is:
- there are 100s of classes to create
- they all share features and/or functionality
- you can’t really make them all dynamic or runtime only, rather they have to be “physical”
- and the most important part: you don’t want to spend the time coding each one with all of the appropriate getters/setters and methods
Let’s look at the Data Model code I created for Siffer, but first let’s look at what we might have to do for just the Address Data Model using the more traditional way:
require 'builder'
class Address
attr_accessor :type, :street, :city, :county, :state_province
attr_accessor :country, :postal_code, :grid_location
def initialize(type, street, city, county, state, country, postal, grid)
raise "Type is mandatory" unless type
@type = type
raise "Street is mandatory" unless street
@street = street
raise "City is mandatory" unless city
@city = city
@county = county
raise "State/Province is mandatory" unless state
@state_province = state
raise "Postal Code is mandatory" unless postal
@postal_code = postal
raise "Country is mandatory" unless country
@country = country
@grid_location = grid
end
def to_xml
xml = Builder::XmlMarkup.new
xml.Address(:type => type) do |body|
body.Street(street)
body.City(city)
body.State(state_province)
body.County(county)
body.Country(country)
body.PostalCode(postal_code)
body.GridLocation(grid_location)
end
xml.target!
end
end
This would be the simplest form of the Address class. Whew. One down and only 100 more to go! Keep in mind though we are missing the conditional feature as well as repeatable attributes. Now the initialize method just got 5-10 lines longer. And consider this: Address contains elements that are themselves Data Models. Look at Street for example. It has it’s own sub elements. Now we can see that just to accomplish the Address Data Model we might be in for a hour or more of just typing. Bleh.
I don’t want to do this for 101 classes. So I sat down for a few minutes and thought about how I would want to write the classes. Let’s begin CDD!
Here is what I came up with:
class Address
attribute :type
element :street, :mandatory
element :city
element :county
element :state_province
element :country
element :postal_code
element :grid_location
end
As you can see, this way I write far few lines. The class definition is very easy to understand and I can control each element for mandatory, conditional and repeatable. I wouldn’t mind doing this for 101 classes!
Now that I have how I want to type each class, I need to implement the code that will make this all work. (a little note about Ruby – it’s beautiful in the way it can accomplish this). First I need a way to capture the method calls attribute and element. Let’s build a module:
module DataElement
module ClassMethods
def element(name, type =
ptional, *conditions)
class_eval "def #{name};@values[:#{name}];end"
class_eval "def #{name}=(value);@values[:#{name}] = value;end"
@mandatory ||= []
@conditional ||= {}
@mandatory << name if type == :mandatory
@conditional[name] = conditions if type == :conditional
end
def attribute(name, default = nil)
@attributes ||= {}
@attributes[name] = default
end
end
def self.included(base)
base.extend ClassMethods
end
end
Now I can make the Address class above include this module to get the attribute and element methods:
class Address
include DataElement
attribute :type
element :street, :mandatory
element :city
element :county
element :state_province
element :country
element :postal_code
element :grid_location
end
That's a good look. The include statement defines the class as a data element. Now piece by piece I'll explain the module.
First the element method receives three arguments. The name of the element, the type (mandatory, conditional, repeatable) and the conditions if it's conditional. With those arguments we can create instance methods for each element. This dynamically builds our getters and setters (no more typing by hand!). Then it stores the mandatory and conditional elements for use later to perform validations. The attribute method does the same thing for the attributes of the Data Model.
But we're missing the way to initialize new instances of these Data models in an automated way. Again - we don't want to write an initialize method for all 101 of these classes. Stick it in the module!
def initialize(values = {})
write_attributes(values)
values = remove_attributes(values)
write(values)
check_mandatory(values)
check_conditional(values)
end
def write_attributes(values)
@attributes ||= {}
unless class_attributes.nil?
class_attributes.each{|k,v| @attributes[k] = v}
attr_values = values.slice(*class_attributes.keys)
attr_values.each{|k,v| @attributes[k] = v}
end
end
def remove_attributes(values)
values.except(*class_attributes.keys) rescue values
end
def write(values)
@values ||= {}
values.each{|k,v| @values[k] = v}
end
def check_mandatory(values)
mandatory.each do |element|
unless values.keys.include?(element)
raise "#{element.to_s.humanize} is mandatory for #{self.class}."
end
end
end
def check_conditional(values)
unless values.keys.any?{|v| conditional.keys.include?(v)}
conditional.each do |element, conditions|
unless conditions.any?{|c| values.keys.include?(c)}
raise "#{element.to_s.humanize} is mandatory for #{self.class}\
if #{conditions.map{|c| c.to_s.humanize}.join(" or ")} is missing"
end
end
end
end
def class_attributes
self.class.instance_variable_get("@attributes")
end
def mandatory
self.class.instance_variable_get("@mandatory")
end
def conditional
self.class.instance_variable_get("@conditional")
end
With these methods now in place we can initialize an Address like this:
@street = Street.new(:line_1 => "13618 W. Port Royale")
@grid = GridLocation.new(:longitude => 3.098, :latitude => 4.566)
@address = Address.new(:type =>"0123",
:street => @street,
:city => "Surprise",
:county => "Maricopa",
:state_province => "AZ",
:country => "US",
:postal_code => 85379,
:grid_location => @grid)
Hey - I snuck in the Street and GridLocation models there didn't I! Here they are:
class Street
include DataElement
element :line_1, :mandatory
element :line_2
element :line_3
element :complex
element :street_number
element :street_prefix
element :street_name
element :street_type
element :street_suffix
element :apartment_type
element :apartment_number_prefix
element :apartment_number
element :apartment_number_suffix
end
class GridLocation
include DataElement
element :longitude
element :latitude
end
And then to get the xml output we need for each of these data models:
def to_xml
xml = Builder::XmlMarkup.new
args = (attributes.nil?) ? self.class.to_s : [self.class.to_s, attributes]
xml.tag!(*args) { |body|
@values.each do |key, value|
if(value.is_a?(DataElement))
body << value.to_xml
else
body.tag!(key.to_s.classify, value)
end
end
}
xml.target!
end
alias :to_s :to_xml
So with all of this now I can create any Data Model I wish like this:
class ConstructionDrivenDesign
include DataElement
attribute :type, "kick-ass"
element :laziness
element :speed, :conditional, :laziness
end
@cdd = ConstructionDrivenDesign.new(:laziness => true)
@cdd_fast = ConstructionDrivenDesign.new(:type => "mach-1", :speed => "fast")
puts @cdd
puts @cdd_fast
<ConstructionDrivenDesign type="kick-ass"><Lazines>true</Lazines></ConstructionDrivenDesign>
<ConstructionDrivenDesign type="mach-1"><Speed>fast</Speed></ConstructionDrivenDesign>
Hopefully this illustrates Construction Driven Design. It's not rocket science. It's just a way for a lazy developer to get a lot done in the fewest keystrokes.