Загрузка...

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

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

4. Metaprogramming patterns — 19 кю. Спасение утопающих дело рук самих утопающих

28.02.09, 16:32
Автор artem.voroztsov

Предположим, что у вас есть библиотечный метод, который иногда кидает ексепшены.
Этот метод библиотечный в том смысле, что вы не хотите трогать руками тот файл, где он определён, так как этот файл, например, относится к библиотеке, которая регулярно обновляется, и ваши изменения после каждого обновления будут теряться, если вы специально не позаботитесь о их сохранении.
Такие методы принято менять в своем собственном коде — в динамических языках можно прямо в своем коде переписать избранный метод избранного класса.

Например:

require 'net/http'
module Net
  class HTTP
    def get(*args)
      # ваш собственный код для этого метода
    end 
  end
end


Такая техника называется monkey patching. Спрашивается, причём здесь обезьяны? Они здесь совершенно не причём. Гораздо ближе к причине гориллы. Но и они тоже не виноваты. А виноваты во всем партизаны! Изначально этот термин назывался «партизанским патчем» (guerrilla patch), английский термин уж очень был похож по звучанию на «патч гориллы», ну а там пошла пьянка и он превратился в «обезьяний патч» (программисты стали друг на друга обижаться, «обезьяна» менее обидна, чем «горилла», на том все и сошлись).

О взаимодействии партизанских отрядов

Как поступают партизаны? Они в тайне, без долгих дипломатических переговоров, предварительных угроз, соглашений об открытии и закрытии гуманитарного коридора, без договоров о правилах ведения войны и др. формальностей начинают действовать.

Партизанских группировок может быть несколько, они часто знают или догадываются о существовании других партизанских группировок, но у них нет единого плана, и, в принципе, одна группировка может решить ограбить поезд с вооружением, а вторая этот же поезд решить взорвать. Последовательность здесь будет важна, но в принципе, необходимая партизанам глобальная цель будет достигнута в любом случае. Неприятно, если поезд взорвется во время грабежа, и погибнут свои же.
В программировании однонитевых приложений одновременность отсутствует как явление, особенно если дело касается инициализации классов — нити принято создавать после того, как сделаны необходимые require и классы динамическим образом созданы/пропатчены. Как поступать с require — отдельный сложный вопрос (1, 2). Лочить require (то есть не давать управления другим нитям, пока не закончится выполнение require) оказывается нельзя, так как может возникнуть dead lock).

Но программисты всё таки не партизаны. Они не грабят вражеские поезда и не взрывают, а создают и улучшают. Так что можно надеяться на лучшее. Постепенно проникаешься культурой партизанских патчей — партизань, но делай это так, чтобы не менялась семантика и сигнатура методов, а то всё к чёрту рассыплется.

Примеры в студию!

Итак, мы можем взять любой метод любого класса и переопределить его. Говорят, что классы в динамических языках открыты.
Ой! каких только подлостей можно понатворить, используя эту открытость:
class Fixnum
  def *(x)
    42
  end
end
puts 5*5
puts 5*14

class Fixnum
  alias orig_div /
  def /(x)
    puts "стук-стук: тут кто-то делит #{self} на #{x}"
    self.orig_div(x)    
  end
end
puts 54/12
puts 13/0

В последнем примере использована языковая конструкция alias, которая сохраняет тело функции под другим именем. Правильнее думать про операцию alias как про операцию копирования тела метода и назначения ему нового имени. Используют alias перед переопределением метода с целью получения доступа к предыдущей непропатченой версии метода по некоторому новому имени.

Такой подход активно используется. Например, можно писать код подобный следующему:
require 'net/http'
class HTTP
  alias get_orig get
  def restore_connection
    begin
      do_start
      true
    rescue 
      false
    end
  end
  def get(*args)
    attempts = 0
    begin
      get_orig(*args, &block)
    rescue Errno::ECONNABORTED => e
      if (attempts += 1) < 3
        restore_connection
        retry
      end
      raise e
    end
  end 
end

Возможно, кому-то этот код покажется прорывом, но меня он не удовлетворяет. Я хочу писать так:
require 'net/http'
class HTTP
  make_rescued :get,
    :rescue => [Errno::ECONNABORTED, Errno::ECONNRESET, EOFError, Timeout::Error],
    :retry_attempts => 3,
    :on_success => lambda{|obj, args, res| puts "We did it!: #{args.inspect}"},
    :sleep_before_retry => 1,
    :ensure => lambda{|obj,args| puts "Finishing :#{args.inspect}" },
    :timeout => 3,
    :retry_if => lambda do |obj, args, e, attempt|
      obj.instance_eval do
        case e
        when Errno::ECONNABORTED, Errno::ECONNRESET
          # сокет! порвали сокет!
          restore_connection
        when EOFError, Timeout::Error
          # что за ерунда? А ну ка еще раз
          true
        end
      end
    end
end

Делать более терпимые к исключительным ситуациям методы — важнейшая, часто возникающая задача. Код, посвящённый обработке исключительных ситуаций, постепенно увеличивает свою долю, и я осмелюсь сказать, что в эвристическом программировании, в веб программировании и, вообще, в современном программировании, он уже составляет 30% или более и является важнейшей компонентой бизнес логики. А раз это важно, почему бы не написать метод общего назначения make_rescued, учитывающий разнообразные опции и решающий задачу спасения в полной мере? Пора делать новый паттерн!

Да-да, паттерны метапрограммирования в Ruby часто представлены в виде методов модифицирующих методы. Другие типичные паттерны — примеси, сами техники использования примесей (extend & include & included), а также метод method_missing. Обо всём этом мы поговорим в следующих топиках.

Приближение 1. Учитываем опции :rescue, :retry_attempts
module MakeRescued
  def extract_options(args)
    args.pop if args.last.is_a?(Hash)
  end
  def alias_method(a, b)
    class_eval "alias #{a} #{b}"
  end
  def make_rescued(*methods)
    options = extract_options(methods)
    exceptions = options[:rescue] || [Exception]
    methods.each do |method|
      method_without_rescue = "#{method}_without_rescue"
      alias_method  method_without_rescue, method
      define_method(method) do |*args|
        retry_attempts = 0
        begin
          send(method_without_rescue, *args)
        rescue Exception => e
          retry_attempts += 1
          unless options[:retry_attempts] && retry_attempts > options[:retry_attempts]
            if exceptions.any?{|klass| klass===e}
              retry
            end
          end
          raise e
        end
      end
    end
  end
end

Приближение 2. Учитываем все предложенные опции

require 'timeout'

module MakeRescued
  def extract_options(args)
     args.last.is_a?(Hash) ? args.pop : {}
  end
  def alias_method(a, b)
    class_eval "alias #{a} #{b}"
  end
  def make_rescued(*methods)
    options = extract_options(methods)
    exceptions = options[:rescue] || [Exception]
    methods.each do |method|
      method_without_rescue = "#{method}_without_rescue"
      alias_method  method_without_rescue, method
      define_method(method) do |*args|
        retry_attempts = 0
        begin
          res = nil
          res = if options[:timeout]
            Timeout::timeout( options[:timeout] ) do
              send(method_without_rescue, *args)
            end
          else
            send(method_without_rescue, *args)
          end
          options[:on_success][self,args,res] if options[:on_success]
          res
        rescue Exception => e
          retry_attempts += 1
          unless options[:retry_attempts] && retry_attempts > options[:retry_attempts]
            if exceptions.any?{|klass| klass===e}
              if options[:retry_if] && options[:retry_if][self,args,e,retry_attempts]
                sleep options[:sleep_before_retry] if options[:sleep_before_retry]
                retry
              end
            end
          end
          options[:on_fail][self,args,e] if options[:on_fail]
          raise e
        ensure
          options[:ensure][self,args,res] if options[:ensure]
          res
        end
      end
    end
  end
end

Module.module_eval { include MakeRescued }

Этот код можно развивать далее. Например, добавить опцию :default, в которой указывается значение метода по умолчанию, если выпадает Exception. Если эта опция равна блоку (есть объект класса Proc), то значит нужно вызывать этот блок с параметрами (self, args) и результат вычисления возвращать как результат метода.

Другие предложения по улучшению метода make_rescued приветствуются.

Классика: alias_method_chain

Это классика monkey patching. Её нужно знать:

и критически к ней подходить:

О методе alias_method_chain мы ещё поговорим. Сейчас лишь отметим, что можно было бы писать так:

...
def get_with_rescue(*args)
...
get_without_rescue(*args)
...
end

alais_method_chain :get, :rescue

где для модулей предварительно определяется метод
def alias_method_chain(target, feature)
  alias_method "#{target}_without_#{feature}", target
  alias_method target, "#{target}_with_#{feature}"
end

Использование нотации именования method_with_feature и method_without_feature позволяет программистам понимать по стеку вызовов, что происходит углубление в пропатченные партизанами методы. При выпадании Exception мы видим значащие имена методов. Кроме того, у нас для каждой фичи есть два метода — с этой фичей и без неё, и иногда возникает необходимость вызывать их непосредственно.
class Module
  def alias_method(a, b)
    class_eval "alias #{a} #{b}"
  end
  def alias_method_chain(target, feature)
    alias_method "#{target}_without_#{feature}", target
    alias_method target, "#{target}_with_#{feature}"
  end
end

# для каждой фичи будет два метода: method_without_feature и method_with_feature
class Abc
  def hello
    puts "hello"
    raise 'Bang!' 
  end
  
  def hello_with_attention
    puts "attention,"
    hello_without_attention
  end
  alias_method_chain :hello, :attention
  
  def hello_with_name(name)
    puts "my darling #{name},"
    hello_without_name
  end
  alias_method_chain :hello, :name
end

Abc.new.hello('Liza')

greck $ ruby method_chain_sample_backtrace.rb
method_chain.rb:14:in `hello_without_attention': Bang! (RuntimeError)
from method_chain.rb:19:in `hello_without_name'
from method_chain.rb:25:in `hello'
from method_chain.rb:30
my darling Liza,
attention,
hello
greck $

Есть и другой важный бонус, который предоставляется такой техникой: различные IDE могут быстро при клике на строчку из backtrace выпавшего ексепшена перекинуть вас на нужную строчку нужного метода, поскольку методы действительно имеют разные имена, в отличие от упрощённого альтернативного подхода, где делаются только методы method_without_feature, а все определения определяют один и тот же метод:
# упрощённый подход, где делаются только методы вида method_without_feature
class Abc
  def hello
    puts "hello"
    raise 'Bang!' 
  end
  
  alias  hello_without_attention hello 
  def hello
    puts "attention,"
    hello_without_attention
  end
  
  alias hello_without_name hello
  def hello(name)
    puts "my darling #{name},"
    hello_without_name
  end
end

Ссылки

  • en.wikipedia.org/wiki/Monkey_patch
  • 1. Metaprogramming patterns — 25кю. Метод eval
  • 2. Metaprogramming patterns — 22кю. Reuse в малом — bang!
  • 3. Metaprogramming patterns — 20 кю. Замыкания

Комментарии

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