Загрузка...

Ruby & Rails: веб-разработка с удовольствием

Ruby on Rails — фреймворк для создания веб-приложений. Является открытым программным обеспечением (лицензия MIT). Здесь мы обсуждаем новости RoR, делимся учебными материалами и интересными находками С RoR даже сложные веб-приложения могут быть написаны за считанные дни. Это действительно разработка с удовольствием!
     

8. Metaprogramming patterns — 18 кю. Методы модификаторы: postprocess_value

26.03.09, 03:24
Автор artem.voroztsov

Рассмотрим сегодня простую вещь: метод postprocess_value, который позволяет указывать лямбду для постпроцессинга результата, возвращаемого некоторым методом.

1. Metaprogramming patterns — 25кю. Метод eval
2. Metaprogramming patterns — 22кю. Reuse в малом — bang!
3. Metaprogramming patterns — 20 кю. Замыкания
4. Metaprogramming patterns 19 кю. Спасение утопающих дело рук самих утопающих
5. Metaprogramming patterns — 18 кю. Примеси
6. Metaprogramming patterns — 17 кю. Как параллелить потоки данных и почему в Ruby не нужны колбэки
7. Metaprogramming patterns — 15 кю. without_feature - управление фичастостью методов

Вот так хотелось бы требовать, чтобы метод Array#index возвращал вместо nil и false значение 0:

class Array
  postprocess_value :index {|value| value || 0}
end

То есть, одно из применений postprocess_value - это определение значения по умолчанию для случаев, когда оригинальный метод возвращает что-то нехорошее.

Примечание. Напомню, что для хешей (экземпляров класса Hash) есть возможность указывать значение по умолчанию непосредственно в конструкторе хеша, например: Hash.new(0). А при конструировании массива нельзя указать значение по умолчанию: несложно создать массив нулей фиксированного размера, например, ary = Array.new(1000) {|idx| 0}, но нет предусмотренной в классе Array возможности сделать так, чтобы вызов ary[число большее максимального индекса] возвращал 0:

a = Array.new(1000) {|idx| 0}
puts a[400]  #=> 0
puts a[2000] #=> nil
a = Hash.new(0)
puts a[2000]  #=> 0
puts a['hello'] #=> 0

Кроме указания значения по умолчанию у postprocess_value есть  и другие применения, например:

class Project < ActiveRecord::Base
  serialize :settings
  postprocess_value :settings {|value| (value || {}).symbolize_keys! }
end

Кстати, полезно в данном примере добавить ещё одну строку с вызовом метода make_rescued, описанного в уроке 4:

class Project < ActiveRecord::Base
  serialize :settings
  make_rescued :settings, :default => {}
  postprocess_value :settings {|value| (value || {}).symbolize_keys! }
end

Итак, приступим к реализации.

def postprocess_value(*methods, &block)
  feature = "post#{block.object_id}"
  methods.each do |method|
    define_method("#{method}_with_#{feature}") do |*args|
      value = send("#{method}_without_#{feature}", *args)
      block[value]
    end
    alias_method_chain method, feature
  end
end

Собственно, это простейший метод-модификатор-методов, и с него можно было бы начать знакомство с alias_method_chain.

Займёмся улучшениями

  1. Для постпроцессинга хотелось бы указывать возможность указывать не только блок, но и имя метода для препроцессинга.
  2. При постпроцессинге полезно иметь доступ к самому объекту, а не только к возвращаемому значению.

Вот более сложные (гипотетические) примеры использования:

class Song
  postprocess_value :title do  |song, value|
    value || "Unknown song of #{obj.artist.title}"
  end
  include FixMisspell
  # Guess misspells and show them for site developers
  postprocess_value :title, :lyrics, :with => :show_misspells, :if => :development?
end
def postprocess_value(*methods, &block)
  options = methods.extract_options!
  block ||= options[:with]
  feature = "post#{block.object_id}"
  methods.each do |method|
    define_method("#{method}_with_#{feature}") do |*args|
      value = send("#{method}_without_#{feature}", *args)
      if options[:if] && !execute_it(options[:if], binding, self, value)
        value
      else
        execute_it(block, binding, self, value)
      end
    end
    alias_method_chain method, feature
  end
end

Определим используемые методы execute_it и extract_options!:

def execute_it(smth, binding, obj, *args)
  case smth
  when String
    eval smth, binding
  when Proc
    smth.call(obj, *args)
  when Symbol
    send(smth, *args)
  else
    raise ArgumentError, "Not executable argument for execute_it"
  end
end
class Array
  def extract_options!
    last.is_a?(Hash) ? pop : {}
  end
end

Здесь возникает интересная потребность - выполнять данный нам Proc с контекстом (то есть с binding), равным контексту внутренности вызываемого метода, а не с тем, в котором этот Proc был создан. Другими словами, хочется, чтобы self внутри блока, который мы пишем, означал объект, для которого был вызван метод.

Указанная потребность пока нами не удовлетворена и не может быть удовлетворена простыми способами. Еть мнение, что невозможность поменять binding у созданного объекта Proc считается полезными ограничением - ограничением, которое делает Ruby более предсказуемым. Динамичность Руби и так уже страшна своей мощью. Иногда даже приятно увидеть, что есть какие то рамки с которыми нужно считаться.

На этот раз всё. Всем большой привет!

Комментарии

Кстати, по аналогии с этим, можно написать метод preprocess_args для предварительной обработки аргументов. Например, вот так:
def preprocess_args(methods, &blk)
feature = “preprocess_#{blk.object_id}”
methods.each do |method|
define_method “#{method}_with_#{feature}” do |
args|
processed_args = blk.call *args
send “#{method}_without_#{feature}”, *processed_args
end
alias_method_chain method, feature
end
end

Мда, код в коментах выглядит не очень =)

:). Привет, Миша. Да, не очень. Нужно работать. Сделаем pre если несколько строк начинаются не с первой позиции

Войдите, чтобы оставить комментарий