Martin Fowler recently blogged about the Embedded Document pattern. It is a very convenient way of dealing with JSON documents in web apps, and we are using it for our models in the app we are currently working on.

In this post, we will see how we can abstract away the smaller patterns that recur when using the Embedded Document pattern. Here is the code snippet we will be working with, based on code from Fowler’s post:

class Delivery
def initialize(data)
@data = data
end
def customer
@data['customer']
end
def ship_date
Date.parse(@data['shipDate'])
end
end
class Order
def initialize(data)
@data = data
end
def deliveries
@data['deliveries'].map { |d| Delivery.new(d) }
end
def quantity_for(a_product)
item = @data['items'].detect { |i| a_product == i['product'] }
item ? item['quantity'] : 0
end
end
order_hash = JSON.parse(
'{
"id": 1234,
"customer": "martin",
"items": [
{"product": "talisker", "quantity": 500},
{"product": "macallan", "quantity": 800},
{"product": "ledaig", "quantity": 1100}
],
"deliveries": [
{ "id": 7722,
"shipDate": "2013-04-19",
"items": [
{"product": "talisker", "quantity": 300},
{"product": "ledaig", "quantity": 500}
]
},
{ "id": 6533,
"shipDate": "2013-04-18",
"items": [
{"product": "talisker", "quantity": 200},
{"product": "ledaig", "quantity": 300},
{"product": "macallan", "quantity": 300}
]
}
]
}'
)
order = Order.new(order_hash)
p order.deliveries
p order.quantity_for('talisker')

We can observe the following recurring patterns in the above code:

  1. All of these classes need to have a constructor that takes a data object and stores it in a field.
  2. Many attributes require only a simple lookup in the wrapped hash, for example Delivery#customer.
  3. Some attributes need a lookup in the hash followed by a transformation, for example Delivery#ship_date.
  4. In some cases, the nested hashes further need to be wrapped in objects. These nested objects can appear in sequences too, for example Order#deliveries.
  5. Some attributes and methods involve complex operations and are too specific to derive any generalisation from them, for example Order#quantity_for.

Let us start by defining a class called EmbeddedDocument, which provides the base for such classes.

class EmbeddedDocument < Struct.new(:data)
end

This provides the necessary constructor to a subclass of EmbeddedDocument, thus taking care of item 1.

Before we go further, let us conjure up an abstraction named “embedder”. An embedder is nothing but an object with a method named call that takes one of the possible types in a JSON document, such as a number, a string, an array, a hash, or nil, and returns a more useful representation thereof, potentially wrapping it in some subclass of EmbeddedDocument.

Now we can define an identity embedder, something that returns a value as-is, without any transformations, which we will use for scalar values such as numbers, strings, and nil.

def scalar
lambda { |x| x }
end

This takes care of item 2.

We can define some common transformations in a similar way and take care of item 3. We will define one for dates to serve as an example.

def date
lambda { |x| Date.parse(x) }
end

We will deal with item 4 in two steps.

Step 1: We will make the class object associated with every EmbeddedDocument an embedder too. That is, it will have a call method that takes parsed data and returns an instance wrapping it.

class EmbeddedDocument < Struct.new(:data)
def self.call(value)
new(value)
end
end

Step 2: We will now define a couple of functions that take one embedder and return another one. This is a very powerful technique from functional programming called combinatory design. What we are defining below can be referred to as embedder combinators.

def sequence_of(e)
lambda { |array|
array.map { |x| e.call(x) }
}
end
def defaulted(e, default)
lambda { |value|
value.nil? ? default : e.call(value)
}
end

Let us define a method key that will allow us to specify a key name and the embedder to be used.

class EmbeddedDocument < Struct.new(:data)
def key(name, embedder)
embedder.call(data[name])
end
def self.call(value)
new(value)
end
end

We now have all the machinery ready. This is how our original classes look with the new utility:

class Delivery < EmbeddedDocument
def customer
key('customer', scalar)
end
def ship_date
key('shipDate', date)
end
end
class Item < EmbeddedDocument
def product
key('product', scalar)
end
def quantity
key('quantity', scalar)
end
end
class Order < EmbeddedDocument
def deliveries
key('deliveries', sequence_of(Delivery))
end
def items
key('items', sequence_of(Item))
end
def quantity_for(a_product)
item = items.detect { |i| a_product == i.product }
item ? item.quantity : 0
end
end

Beautiful, isn’t it?

But this is Ruby, and we can do even better. Here are a couple of ways we can improve the solution above:

  1. Define a method on EmbeddedDocument’s class that takes only the absolute essentials and defines a JSON accessor method for us.
  2. We have many cases above in which a field access is a simple lookup in the wrapped hash and nothing more. Why not put method_missing to good use here?

Here is what our EmbeddedDocument class looks like after those improvements:

class EmbeddedDocument < Struct.new(:data)
def self.key(name, embedder, accessor_name = nil)
name = name.to_s
accessor_name ||= name
define_method(accessor_name) do
embedder.call(data[name])
end
end
def self.call(value)
new(value)
end
def method_missing(sym)
data[sym.to_s]
end
end

Use:

class Item < EmbeddedDocument
end
class Delivery < EmbeddedDocument
key :shipDate, date, :ship_date
key :items, sequence_of(Item)
end
class Order < EmbeddedDocument
key :deliveries, sequence_of(Delivery)
key :items, sequence_of(Item)
def quantity_for(a_product)
item = items.detect { |i| a_product == i.product }
item ? item.quantity : 0
end
end

This is even more concise, and I think it is an improvement on the earlier version.