4. Metaprogramming patterns. 19 кю. Спасение утопающих дело рук самих утопающих
Ruby*
Предположим, что у вас есть библиотечный метод, который иногда кидает ексепшены.
Этот метод библиотечный в том смысле, что вы не хотите трогать руками тот файл, где он определён, так как этот файл, например, относится к библиотеке, которая регулярно обновляется, и ваши изменения после каждого обновления будут теряться, если вы специально не позаботитесь о их сохранении.
Такие методы принято менять в своем собственном коде — в динамических языках можно прямо в своем коде переписать избранный метод избранного класса. Например:
Такая техника называется monkey patching. Спрашивается, причём здесь обезьяны? Они здесь совершенно не причём. Гораздо ближе к причине гориллы. Но и они тоже не виноваты. А виноваты во всем партизаны! Изначально этот термин назывался «партизанским патчем» (guerrilla patch), английский термин уж очень был похож по звучанию на «патч гориллы», ну а там пошла пьянка и он превратился в «обезьяний патч» (программисты стали друг на друга обижаться, «обезьяна» менее обидна, чем «горилла», на том все и сошлись).
Как поступают партизаны? Они в тайне, без долгих дипломатических переговоров, предварительных угроз, соглашений об открытии и закрытии гуманитарного коридора, без договоров о правилах ведения войны и др. формальностей начинают действовать.
Партизанских группировок может быть несколько, они часто знают или догадываются о существовании других партизанских группировок, но у них нет единого плана, и, в принципе, одна группировка может решить ограбить поезд с вооружением, а вторая этот же поезд решить взорвать. Последовательность здесь будет важна, но в принципе, необходимая партизанам глобальная цель будет достигнута в любом случае. Неприятно, если поезд взорвется во время грабежа, и погибнут свои же.
В программировании однонитевых приложений одновременность отсутствует как явление, особенно если дело касается инициализации классов — нити принято создавать после того, как сделаны необходимые
Но программисты все таки не партизаны. Они не грабят вражеские поезда и не взрывают, а создают и улучшают. Так что можно надеяться на лучшее. Постепенно проникаешься культурой партизанских патчей — партизань, но делай это так, чтобы не менялась семантика и сигнатура методов, а то всё к чёрту рассыплется.
Итак, мы можем взять любой метод любого класса и переопределить его. Говорят, что классы в динамических языках открыты.
Ой, каких только подлостей можно понатворить, используя эту открытость:
В последнем примере использована языковая конструкция
Такой подход активно применяется на практике. Например, можно писать код подобный следующему:
Возможно, кому-то этот код покажется прорывом, но меня он не удовлетворяет. Я хочу писать так:
Делать более терпимые к исключительным ситуациям методы — важнейшая, часто возникающая задача. Код, посвящённый обработке исключительных ситуаций, постепенно увеличивает свою долю, и я осмелюсь сказать, что в эвристическом программировании, в веб программировании и, вообще, в современном программировании, он уже составляет 30% или более и является важнейшей компонентой бизнес логики. А раз это важно, почему бы не написать метод общего назначения
Да-да, паттерны метапрограммирования в Ruby часто представлены в виде методов модифицирующих методы. Другие типичные паттерны — примеси, сами техники использования примесей (extend & include & included), а также метод
Приближение 1. Учитываем опции :rescue, :retry_attempts
Приближение 2. Учитываем все предложенные опции
Этот код можно развивать далее. Например, добавить опцию
Другие предложения по улучшению метода
Классика:
Это классика monkey patching. Её нужно знать:
и критически к ней подходить:
О методе
где для модулей предварительно определяется метод
Использование нотации именования
Есть и другой важный бонус, который предоставляется такой техникой: различные IDE могут быстро при клике на строчку из backtrace выпавшего ексепшена перекинуть вас на нужную строчку нужного метода, поскольку методы действительно имеют разные имена, в отличие от упрощённого альтернативного подхода, где делаются только методы
Этот метод библиотечный в том смысле, что вы не хотите трогать руками тот файл, где он определён, так как этот файл, например, относится к библиотеке, которая регулярно обновляется, и ваши изменения после каждого обновления будут теряться, если вы специально не позаботитесь о их сохранении.
Такие методы принято менять в своем собственном коде — в динамических языках можно прямо в своем коде переписать избранный метод избранного класса. Например:
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. Её нужно знать:
- weblog.rubyonrails.org/2006/4/26/new-in-rails-module-alias_method_chain
- errtheblog.com/posts/48-aliasmethodbling
и критически к ней подходить:
О методе
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

комментарии (7)