📜 ⬆️ ⬇️

4. Metaprogramming patterns. 19 Kyu. Salvation of drowning business of the hands of drowning people

Suppose that you have a library method that sometimes throws operations.
This is a library method in the sense that you do not want to touch the file where it is defined, since this file, for example, refers to a library that is regularly updated, and your changes after each update will be lost if you do not specifically take care of their preservation.
Such methods are usually changed in your own code - in dynamic languages ​​you can directly in your code rewrite the selected method of the selected class. For example:

require ' net/http ' <br>
module Net <br>
class HTTP <br>
def get (*args)<br>
# <br>
end <br>
end <br>
end <br>
<br>

This technique is called monkey patching . The question is, where are the monkeys? They have nothing to do with it. Much closer to the cause of the gorilla. But they are also not to blame. And the guerrillas are to blame for everything! Initially, this term was called “guerrilla patch”, the English term was very similar in sound to the “gorilla patch”, well, and then the booze went and it turned into a “monkey patch” (the programmers began to be offended at each other, “the monkey "Less offensive than the" gorilla ", that's all agreed).

On the interaction of guerrilla groups


What do the partisans do? They are in secret, without long diplomatic negotiations, preliminary threats, agreements on the opening and closing of the humanitarian corridor, without agreements on the rules of warfare and other formalities begin to act.
')
There can be several guerrilla groups, they often know or guess about the existence of other guerrilla groups, but they do not have a single plan, and, in principle, one group can decide to rob a train with armaments, and the second one can decide to blow it up. The consistency here will be important, but in principle, the global goal needed by the partisans will be achieved anyway. It is unpleasant if the train explodes during the robbery, and their own will die.
In programming single-threaded applications, simultaneity is absent as a phenomenon, especially when it comes to class initialization - it is customary to create threads after the necessary require and classes are created dynamically created / patched. How to deal with require is a separate complex question ( 1 , 2 ). It is not possible to lock the require (that is, not to give control to other threads until the require execution ends), since a dead lock may occur.

But programmers are still not partisans. They do not rob enemy trains and do not blow up, but create and improve. So you can hope for the best. Gradually you get into the culture of partisan patches — partisans, but do it in such a way that the semantics and signature of methods do not change, otherwise it will crumble.

Examples in the studio!


So, we can take any method of any class and override it. It is said that classes in dynamic languages ​​are open.
Oh, what kind of tricks you can pate using this openness:
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

The last example uses the alias language construct, which stores the function body under a different name. It is more correct to think about the alias operation as about the operation of copying the method body and assigning a new name to it. Use alias before redefining the method in order to gain access to the previous unpatched version of the method by the selected new name.

This approach is actively used in practice. For example, you can write code like this:
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

This code may seem like a breakthrough to someone, but it does not satisfy me. I want to write like this:
<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>

Making methods more tolerant of exceptional situations is the most important, often arising task. The code dedicated to handling exceptional situations gradually increases its share, and I dare say that in heuristic programming, in web programming and, in general, in modern programming, it already amounts to 30% or more and is an essential component of business logic. And since this is important, why not write the general-purpose method make_rescued , which takes into account various options and solves the problem of salvation in full? It's time to make a new pattern!

Yes, Ruby metaprogramming patterns are often represented as modifying methods. Other typical patterns are impurities, impurity use techniques themselves (extend & include & included), and method_missing method. All this we will discuss in the following topics.

Approximation 1. We consider options: 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>


Approximation 2. We consider all the proposed options.

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 }


This code can be developed further. For example, add the :default option, which specifies the default value of the method if Exception dropped. If this option is equal to a block (there is an object of class Proc ), then it means you need to call this block with parameters (self, args) and return the result of the calculation as a result of the method.

Other suggestions for improving the make_rescued method make_rescued welcome.

Classic: alias_method_chain


This is a classic monkey patching. She needs to know:

and critically approach it:


We will talk about the alias_method_chain method. Now we only note that it would be possible to write this:
 ...
   def get_with_rescue (* args)
     ...
       get_without_rescue (* args)
     ...
   end

   alais_method_chain: get,: rescue

where for modules the method is predefined
def alias_method_chain (target, feature)<br>
alias_method " #{ target } _without_ #{ feature } " , target<br>
alias_method target, " #{ target } _with_ #{ feature } " <br>
end <br>

Using the method_with_feature and method_without_feature naming notation allows programmers to understand by the call stack that there is a deepening in the methods patched by the partisans. When Exception dropped, we see significant method names. In addition, we have for each feature two methods - with this feature and without it, and sometimes there is a need to call them directly.
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 $ 

There is another important bonus that is provided by such a technique: different IDEs can quickly throw you onto the right line of the desired method when clicking on a line from the backtrace of the drop-down action, because the methods do have different names, in contrast to the simplified alternative approach, where only method_without_feature methods are method_without_feature , and all definitions define the same method:
<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>



Links


Source: https://habr.com/ru/post/50819/


All Articles