Создание билдеров (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
