Предположим, что у вас есть библиотечный метод, который иногда кидает ексепшены.
Этот метод библиотечный в том смысле, что вы не хотите трогать руками тот файл, где он определён, так как этот файл, например, относится к библиотеке, которая регулярно обновляется, и ваши изменения после каждого обновления будут теряться, если вы специально не позаботитесь о их сохранении.
Такие методы принято менять в своем собственном коде — в динамических языках можно прямо в своем коде переписать избранный метод избранного класса. Например:
Такая техника называется monkey patching. Спрашивается, причём здесь обезьяны? Они здесь совершенно не причём. Гораздо ближе к причине гориллы. Но и они тоже не виноваты. А виноваты во всем партизаны! Изначально этот термин назывался «партизанским патчем» (guerrilla patch), английский термин уж очень был похож по звучанию на «патч гориллы», ну а там пошла пьянка и он превратился в «обезьяний патч» (программисты стали друг на друга обижаться, «обезьяна» менее обидна, чем «горилла», на том все и сошлись).
Как поступают партизаны? Они в тайне, без долгих дипломатических переговоров, предварительных угроз, соглашений об открытии и закрытии гуманитарного коридора, без договоров о правилах ведения войны и др. формальностей начинают действовать.
Партизанских группировок может быть несколько, они часто знают или догадываются о существовании других партизанских группировок, но у них нет единого плана, и, в принципе, одна группировка может решить ограбить поезд с вооружением, а вторая этот же поезд решить взорвать. Последовательность здесь будет важна, но в принципе, необходимая партизанам глобальная цель будет достигнута в любом случае. Неприятно, если поезд взорвется во время грабежа, и погибнут свои же.
В программировании однонитевых приложений одновременность отсутствует как явление, особенно если дело касается инициализации классов — нити принято создавать после того, как сделаны необходимые
Но программисты все таки не партизаны. Они не грабят вражеские поезда и не взрывают, а создают и улучшают. Так что можно надеяться на лучшее. Постепенно проникаешься культурой партизанских патчей — партизань, но делай это так, чтобы не менялась семантика и сигнатура методов, а то всё к чёрту рассыплется.
Итак, мы можем взять любой метод любого класса и переопределить его. Говорят, что классы в динамических языках открыты.
Ой, каких только подлостей можно понатворить, используя эту открытость:
В последнем примере использована языковая конструкция
Такой подход активно применяется на практике. Например, можно писать код подобный следующему:
Возможно, кому-то этот код покажется прорывом, но меня он не удовлетворяет. Я хочу писать так:
Делать более терпимые к исключительным ситуациям методы — важнейшая, часто возникающая задача. Код, посвящённый обработке исключительных ситуаций, постепенно увеличивает свою долю, и я осмелюсь сказать, что в эвристическом программировании, в веб программировании и, вообще, в современном программировании, он уже составляет 30% или более и является важнейшей компонентой бизнес логики. А раз это важно, почему бы не написать метод общего назначения
Да-да, паттерны метапрограммирования в Ruby часто представлены в виде методов модифицирующих методы. Другие типичные паттерны — примеси, сами техники использования примесей (extend & include & included), а также метод
Приближение 1. Учитываем опции :rescue, :retry_attempts
Приближение 2. Учитываем все предложенные опции
Этот код можно развивать далее. Например, добавить опцию
Другие предложения по улучшению метода
Классика:
Это классика monkey patching. Её нужно знать:
и критически к ней подходить:
О методе
где для модулей предварительно определяется метод
Использование нотации именования
Есть и другой важный бонус, который предоставляется такой техникой: различные IDE могут быстро при клике на строчку из backtrace выпавшего ексепшена перекинуть вас на нужную строчку нужного метода, поскольку методы действительно имеют разные имена, в отличие от упрощённого альтернативного подхода, где делаются только методы
Этот метод библиотечный в том смысле, что вы не хотите трогать руками тот файл, где он определён, так как этот файл, например, относится к библиотеке, которая регулярно обновляется, и ваши изменения после каждого обновления будут теряться, если вы специально не позаботитесь о их сохранении.
Такие методы принято менять в своем собственном коде — в динамических языках можно прямо в своем коде переписать избранный метод избранного класса. Например:
require 'net/http'<br>
module Net<br>
class HTTP<br>
def get(*args)<br>
# ваш собственный код для этого метода<br>
end <br>
end<br>
end<br>
<br>
Такая техника называется monkey patching. Спрашивается, причём здесь обезьяны? Они здесь совершенно не причём. Гораздо ближе к причине гориллы. Но и они тоже не виноваты. А виноваты во всем партизаны! Изначально этот термин назывался «партизанским патчем» (guerrilla patch), английский термин уж очень был похож по звучанию на «патч гориллы», ну а там пошла пьянка и он превратился в «обезьяний патч» (программисты стали друг на друга обижаться, «обезьяна» менее обидна, чем «горилла», на том все и сошлись).
О взаимодействии партизанских отрядов
Как поступают партизаны? Они в тайне, без долгих дипломатических переговоров, предварительных угроз, соглашений об открытии и закрытии гуманитарного коридора, без договоров о правилах ведения войны и др. формальностей начинают действовать.
Партизанских группировок может быть несколько, они часто знают или догадываются о существовании других партизанских группировок, но у них нет единого плана, и, в принципе, одна группировка может решить ограбить поезд с вооружением, а вторая этот же поезд решить взорвать. Последовательность здесь будет важна, но в принципе, необходимая партизанам глобальная цель будет достигнута в любом случае. Неприятно, если поезд взорвется во время грабежа, и погибнут свои же.
В программировании однонитевых приложений одновременность отсутствует как явление, особенно если дело касается инициализации классов — нити принято создавать после того, как сделаны необходимые
require
и классы динамическим образом созданы/пропатчены. Как поступать с require
— отдельный сложный вопрос (1, 2). Лочить require
(то есть не давать управления другим нитям, пока не закончится выполнение require
) оказывается нельзя, так как может возникнуть dead lock).Но программисты все таки не партизаны. Они не грабят вражеские поезда и не взрывают, а создают и улучшают. Так что можно надеяться на лучшее. Постепенно проникаешься культурой партизанских патчей — партизань, но делай это так, чтобы не менялась семантика и сигнатура методов, а то всё к чёрту рассыплется.
Примеры в студию!
Итак, мы можем взять любой метод любого класса и переопределить его. Говорят, что классы в динамических языках открыты.
Ой, каких только подлостей можно понатворить, используя эту открытость:
class Fixnum<br>
def *(x)<br>
42<br>
end<br>
end<br>
puts 5*5<br>
puts 5*14<br>
<br>
class Fixnum<br>
alias orig_div /<br>
def /(x)<br>
puts "стук-стук: тут кто-то делит #{self} на #{x}"<br>
self.orig_div(x) <br>
end<br>
end<br>
puts 54/12<br>
puts 13/ 0
В последнем примере использована языковая конструкция
alias
, которая сохраняет тело функции под другим именем. Правильнее думать про операцию alias
как про операцию копирования тела метода и назначения ему нового имени. Используют alias
перед переопределением метода с целью получения доступа к предыдущей непропатченой версии метода по выбранному новому имени.Такой подход активно применяется на практике. Например, можно писать код подобный следующему:
require 'net/http'<br>
class HTTP<br>
alias get_orig get<br>
def restore_connection<br>
begin<br>
do_start<br>
true<br>
rescue <br>
false<br>
end<br>
end<br>
def get(*args)<br>
attempts = 0 <br>
begin<br>
get_orig(*args, &block)<br>
rescue Errno::ECONNABORTED => e<br>
if (attempts += 1) < 3<br>
restore_connection<br>
retry<br>
end<br>
raise e<br>
end<br>
end <br>
end
Возможно, кому-то этот код покажется прорывом, но меня он не удовлетворяет. Я хочу писать так:
<br>
require 'net/http'<br>
class HTTP<br>
make_rescued :get,<br>
:rescue => [Errno::ECONNABORTED, Errno::ECONNRESET, EOFError, Timeout::Error],<br>
:retry_attempts => 3,<br>
:on_success => lambda{|obj, args, res| puts "We did it!: #{args.inspect}"},<br>
:sleep_before_retry => 1,<br>
:ensure => lambda{|obj,args| puts "Finishing :#{args.inspect}" },<br>
:timeout => 3,<br>
:retry_if => lambda do |obj, args, e, attempt|<br>
obj.instance_eval do<br>
case e<br>
when Errno::ECONNABORTED, Errno::ECONNRESET<br>
# сокет! порвали сокет! <br>
restore_connection<br>
when EOFError, Timeout::Error<br>
# что за ерунда? А ну ка еще раз<br>
true<br>
end<br>
end<br>
end<br>
end<br>
<br>
Делать более терпимые к исключительным ситуациям методы — важнейшая, часто возникающая задача. Код, посвящённый обработке исключительных ситуаций, постепенно увеличивает свою долю, и я осмелюсь сказать, что в эвристическом программировании, в веб программировании и, вообще, в современном программировании, он уже составляет 30% или более и является важнейшей компонентой бизнес логики. А раз это важно, почему бы не написать метод общего назначения
make_rescued
, учитывающий разнообразные опции и решающий задачу спасения в полной мере? Пора делать новый паттерн! Да-да, паттерны метапрограммирования в Ruby часто представлены в виде методов модифицирующих методы. Другие типичные паттерны — примеси, сами техники использования примесей (extend & include & included), а также метод
method_missing
. Обо всём этом мы поговорим в следующих топиках. Приближение 1. Учитываем опции :rescue, :retry_attempts
module MakeRescued<br>
def extract_options(args)<br>
args.pop if args.last.is_a?(Hash)<br>
end<br>
def alias_method(a, b)<br>
class_eval "alias #{a} #{b}"<br>
end<br>
def make_rescued(*methods)<br>
options = extract_options(methods)<br>
exceptions = options[:rescue] || [Exception]<br>
methods.each do |method|<br>
method_without_rescue = "#{method}_without_rescue"<br>
alias_method method_without_rescue, method<br>
define_method(method) do |*args|<br>
retry_attempts = 0 <br>
begin<br>
send(method_without_rescue, *args)<br>
rescue Exception => e<br>
retry_attempts += 1<br>
unless options[:retry_attempts] && retry_attempts > options[:retry_attempts]<br>
if exceptions.any?{|klass| klass===e}<br>
retry<br>
end<br>
end<br>
raise e<br>
end<br>
end<br>
end<br>
end<br>
end<br>
<br>
Приближение 2. Учитываем все предложенные опции
require 'timeout'<br>
<br>
module MakeRescued<br>
def extract_options(args)<br>
args.last.is_a?(Hash) ? args.pop : {}<br>
end<br>
def alias_method(a, b)<br>
class_eval "alias #{a} #{b}"<br>
end<br>
def make_rescued(*methods)<br>
options = extract_options(methods)<br>
exceptions = options[:rescue] || [Exception]<br>
methods.each do |method|<br>
method_without_rescue = "#{method}_without_rescue"<br>
alias_method method_without_rescue, method<br>
define_method(method) do |*args|<br>
retry_attempts = 0 <br>
begin<br>
res = nil<br>
res = if options[:timeout]<br>
Timeout::timeout( options[:timeout] ) do<br>
send(method_without_rescue, *args)<br>
end<br>
else<br>
send(method_without_rescue, *args)<br>
end<br>
options[:on_success][self,args,res] if options[:on_success]<br>
res<br>
rescue Exception => e<br>
retry_attempts += 1<br>
unless options[:retry_attempts] && retry_attempts > options[:retry_attempts]<br>
if exceptions.any?{|klass| klass===e}<br>
if options[:retry_if] && options[:retry_if][self,args,e,retry_attempts]<br>
sleep options[:sleep_before_retry] if options[:sleep_before_retry]<br>
retry<br>
end<br>
end<br>
end<br>
options[:on_fail][self,args,e] if options[:on_fail]<br>
raise e<br>
ensure<br>
options[:ensure][self,args,res] if options[:ensure]<br>
res<br>
end<br>
end<br>
end<br>
end<br>
end<br>
<br>
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)<br>
alias_method "#{target}_without_#{feature}", target<br>
alias_method target, "#{target}_with_#{feature}"<br>
end<br>
Использование нотации именования
method_with_feature
и method_without_feature
позволяет программистам понимать по стеку вызовов, что происходит углубление в пропатченные партизанами методы. При выпадании Exception
мы видим значащие имена методов. Кроме того, у нас для каждой фичи есть два метода — с этой фичей и без неё, и иногда возникает необходимость вызывать их непосредственно.class Module<br>
def alias_method(a, b)<br>
class_eval "alias #{a} #{b}"<br>
end<br>
def alias_method_chain(target, feature)<br>
alias_method "#{target}_without_#{feature}", target<br>
alias_method target, "#{target}_with_#{feature}"<br>
end<br>
end<br>
<br>
# для каждой фичи будет два метода: method_without_feature и method_with_feature<br>
class Abc<br>
def hello<br>
puts "hello"<br>
raise 'Bang!' <br>
end<br>
<br>
def hello_with_attention<br>
puts "attention,"<br>
hello_without_attention<br>
end<br>
alias_method_chain :hello, :attention<br>
<br>
def hello_with_name(name)<br>
puts "my darling #{name},"<br>
hello_without_name<br>
end<br>
alias_method_chain :hello, :name<br>
end<br>
<br>
Abc.new.hello('Liza')<br>
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
, а все определения определяют один и тот же метод:<br>
# упрощённый подхода, где делаются только методы вида method_without_feature<br>
class Abc<br>
def hello<br>
puts "hello"<br>
raise 'Bang!' <br>
end<br>
<br>
alias hello_without_attention hello <br>
def hello<br>
puts "attention,"<br>
hello_without_attention<br>
end<br>
<br>
alias hello_without_name hello<br>
def hello(name)<br>
puts "my darling #{name},"<br>
hello_without_name<br>
end<br>
end<br>
<br>