Given a complex object hierarchy, which luckily contains no cyclic references, how do I implement serialization with support for various formats? I'm not here to discuss an actual implementation. Instead, I'm looking for hints on design patterns which might come in handy.
To be a little more precise: I'm using Ruby and I want to parse XML and JSON data to build up a complex object hierarchy. Furthermore, it should be possible to serialize this hierarchy to JSON, XML, and possibly HTML.
Can I utilize the Builder pattern for this? In any of the mentioned cases, I have some sort of structured data - either in memory or textual - which I want to use to build up something else.
I think it would be nice to separate the serialization logic from the actual business logic, so that I can easily support multiple XML formats later on.
I ended up creating a solution which is based on the Builder and the Strategy pattern. I'm using the Builder pattern to extract parsing and building logic into there own classes. This allows me to easily add new parsers and builders, respectively. I'm using the Strategy pattern to implement the individual parsing and building logic, because this logic depends on my input and output format.
The figure below shows a UML diagram of my solution.
The listing below shows my Ruby implementation. The implementation is somewhat trivial because the object I'm building is fairly simple. For those of you who think that this code is bloated and Java-ish, I think this is actually good design. I admit, in such a trivial case I could have just build the construction methods directly into my business object. However, I'm not constructing fruits in my other application, but fairly complex objects instead, so separating parsing, building, and business logic seems like a good idea.
require 'nokogiri'
require 'json'
class Fruit
attr_accessor :name
attr_accessor :size
attr_accessor :color
def initialize(attrs = {})
self.name = attrs[:name]
self.size = attrs[:size]
self.color = attrs[:color]
end
def to_s
"#{size} #{color} #{name}"
end
end
class FruitBuilder
def self.build(opts = {}, &block)
builder = new(opts)
builder.instance_eval(&block)
builder.result
end
def self.delegate(method, target)
method = method.to_sym
target = target.to_sym
define_method(method) do |*attrs, &block|
send(target).send(method, *attrs, &block)
end
end
end
class FruitObjectBuilder < FruitBuilder
attr_reader :fruit
delegate :name=, :fruit
delegate :size=, :fruit
delegate :color=, :fruit
def initialize(opts = {})
@fruit = Fruit.new
end
def result
@fruit
end
end
class FruitXMLBuilder < FruitBuilder
attr_reader :document
def initialize(opts = {})
@document = Nokogiri::XML::Document.new
end
def name=(name)
add_text_node(root, 'name', name)
end
def size=(size)
add_text_node(root, 'size', size)
end
def color=(color)
add_text_node(root, 'color', color)
end
def result
document.to_s
end
private
def add_text_node(parent, name, content)
text = Nokogiri::XML::Text.new(content, document)
element = Nokogiri::XML::Element.new(name, document)
element.add_child(text)
parent.add_child(element)
end
def root
document.root ||= create_root
end
def create_root
document.add_child(Nokogiri::XML::Element.new('fruit', document))
end
end
class FruitJSONBuilder < FruitBuilder
attr_reader :fruit
def initialize(opts = {})
@fruit = Struct.new(:name, :size, :color).new
end
delegate :name=, :fruit
delegate :size=, :fruit
delegate :color=, :fruit
def result
Hash[*fruit.members.zip(fruit.values).flatten].to_json
end
end
class FruitParser
attr_reader :builder
def initialize(builder)
@builder = builder
end
def build(*attrs, &block)
builder.build(*attrs, &block)
end
end
class FruitObjectParser < FruitParser
def parse(other_fruit)
build do |fruit|
fruit.name = other_fruit.name
fruit.size = other_fruit.size
fruit.color = other_fruit.color
end
end
end
class FruitXMLParser < FruitParser
def parse(xml)
document = Nokogiri::XML(xml)
build do |fruit|
fruit.name = document.xpath('/fruit/name').first.text.strip
fruit.size = document.xpath('/fruit/size').first.text.strip
fruit.color = document.xpath('/fruit/color').first.text.strip
end
end
end
class FruitJSONParser < FruitParser
def parse(json)
attrs = JSON.parse(json)
build do |fruit|
fruit.name = attrs['name']
fruit.size = attrs['size']
fruit.color = attrs['color']
end
end
end
# -- Main program ----------------------------------------------------------
p = FruitJSONParser.new(FruitXMLBuilder)
puts p.parse('{"name":"Apple","size":"Big","color":"Red"}')
p = FruitXMLParser.new(FruitObjectBuilder)
puts p.parse('<fruit><name>Apple</name><size>Big</size><color>Red</color></fruit>')
p = FruitObjectParser.new(FruitJSONBuilder)
puts p.parse(Fruit.new(:name => 'Apple', :color => 'Red', :size => 'Big'))
Anytime someone says they want to do the same operation using different algorithms, and choose which algorithm to use at runtime, the Strategy pattern always comes to mind. The different types of serialization (XML, JSON, binary, whatever) are all different strategies for converting an object to a purely-data, more portable structure. Seems like this might be applicable to your situation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With