Создание билдеров (builders) в Ruby

Давно я не писал ничего, постараюсь исправиться…

Сегодня я рассмотрю создание в Ruby билдера – подобного тому, который строит разметку (http://builder.rubyforge.org/), но чуть попроще. В частности, строить мы будем произвольные Ruby-объекты.

Как это должно выглядеть? В идеале вот так:


obj = TestObj.build
  first_prop 11
  second_prop 'qwerty'
end
obj # <TestObj:0x7fd655d04958 @first_prop=11 @second_prop="qwerty">

Попробуем получить такой результат для произвольного класса.

Для начала попробуем сделать что-нибудь попроще – например, создать класс ObjectBuilder, который позволяет устанавливать из блока свойства объекта:

  • Поскольку такой билдер должен хранить в себе ссылку на объект мы унаследуем его от структуры с одним свойством object.
  • Метод build будет вызывать переданный блок, передавая туда ссылку на билдер. Внутри блока мы будем обращаться к методам билдера, устанавливающим свойства объекта.
  • Вместо того, чтобы описывать по методу для каждого объекта мы переопределим method_missing в котором все неизвестные методы будем направлять объекту.
    Получившийся класс:

class ObjectBuilder < Struct.new( :object )

  def build
    yield self
  end

  def method_missing( name, *args, &block )
    object.send "#{name}=", *args, &block
  end

end

Для использования такого класса сначала необходимо создать его экземпляр, передав туда объект, свойства которого должны быть установлены, затем вызвать метод build с соответствующим блоком, а затем получить объект назад:


class A
  attr_accessor :a, :b, :c
end

builder = ObjectBuilder.new A.new
builder.build do |b|
  b.a 11
  b.c 12
end
builder.object # <A:0x7fd655d04958 @a=11 @c=12>

Поскольку мы перегрузили method_missing еще стоит перегрузить метод respond_to?. В нашем примере это ни на что не повляет, конечно, но пригодится если мы захотим проверять в рантайме какие методы поддерживает билдер. Итак:


  def respond_to?( name, include_private = false )
    super || object.respond_to?( "#{name}=", include_private )
  end

Теперь начнем улучшать наш код. Сначала неплохо бы избавиться от b. – для этого необходимо изменить метод build, и вместо передачи в блок билдера вызывать его (блок) в контексте билдера. Для этого используется метод instance_eval:


  def build( &block )
    instance_eval &block
  end

Теперь можно писать следующий код:


builder = ObjectBuilder.new A.new
builder.build do
  a 11
  c 12
end
builder.object # <A:0x7fd655d04958 @a=11 @c=12>

Уже более-менее похоже на то, что хотелось получить. Осталось убрать явное создание билдера и получение построенного объекта. Для этого выделим вышеприведенный код в отдельный модуль, которым будем расширять класс:


module BuildingSupport

  def build( &block )
    builder = ObjectBuilder.new self.new
    builder.build &block
    builder.object
  end

end

Теперь если мы расширим класс этим модулем, то получим как раз необходимую функциональность:


class A
  extend BuildingSupport

  attr_accessor :a, :b, :c
end

a = A.build
  a 11
  c 12
end
a # <A:0x7fd655d04958 @a=11 @c=12>

Или даже так (здесь все чуть сложнее, мне пришлось переопределить client=):


class Client
  extend BuildingSupport

  attr_accessor :name, :address, :phone
end

class Order
  extend BuildingSupport

  attr_accessor :client, :name, :amount

  def client=( c=nil, &block )
    @client = c || Client.build( &block )
  end
end

order = Order.build do
  name "Book"
  amount 3
  client do
    name "Client name"
    address "Client address"
    phone "000-000-000"
  end
end

 Подписаться на RSS

 #  #  #  #  #  #  #  #  #  #

blog comments powered by Disqus