Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are there any patterns for serializing and deserializing object hierarchies in various formats?

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.

like image 358
t6d Avatar asked Jul 18 '11 13:07

t6d


2 Answers

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.

Parser/Builder Model

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'))
like image 70
t6d Avatar answered Sep 28 '22 02:09

t6d


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.

like image 39
mikemanne Avatar answered Sep 28 '22 00:09

mikemanne