Application example analysis of hook method in Ruby

Time:2021-11-25

By using the hook method, we can intervene in the life cycle of ruby classes or modules, which can greatly improve the flexibility of programming.
Hook methods related to life cycle are as follows:

Class is module dependent

  • Class#inherited
  • Module#include
  • Module#prepended
  • Module#extend_object
  • Module#method_added
  • Module#method_removed
  • Module#method_undefined

Single piece related

  • BasicObject#singleton_method_added
  • BasicObject#singleton_method_removed
  • BasicObject#singleton_method_undefined

Sample code

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
module M1
  def self.included(othermod)
    puts “M1 was included into #{othermod}”
  end
end
 
module M2
  def self.prepended(othermod)
    puts “M2 was prepended to #{othermod}”
  end
end
 
class C
  include M1
  include M2
end
 
#Output
M1 was included into C
M2 was prepended to C
 
module M
  def self.method_added(method)
    puts “New method: M##{method}”
  end
 
  def my_method; end
end
 
#Output
New method: M#my_method

In addition to the methods listed above, you can also rewrite a method of the parent class, perform some filtering operations, and then call the super method to complete the function of the original function, so as to achieve the effect similar to the hook method. Similarly, the surrounding alias can also be used as an alternative implementation of a hook method.

Application examples
Task description:

Write an operation method similar to attr_ Attr of accessor_ Checked class macro, which is used to check the attribute value. The usage method is as follows:

?
1
2
3
4
5
6
7
8
9
10
11
class Person
 include CheckedAttributes
 
 attr_checked :age do |v|
  v >= 18
 end
end
 
me = Person.new
me.age = 39 #ok
me.age = 12 #Throw exception

Implementation plan:

Use the eval method to write a program called add_ checked_ The kernel method of attribute adds a simply verified attribute to the specified class
Refactoring add_ checked_ Attribute method, remove the eval method and use other means to implement it
Add code block verification function
Modify add_ checked_ Attribute is the required attr_ Checked and made available to all classes
By introducing a module, you can only add attr to the class that introduces the function module_ Checked method
Step 1

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def add_checked_attribute(klass, attribute)
 eval "
  class #{klass}
   def #{attribute}=(value)
    raise 'Invalid attribute' unless value
    @#{attribute} = value
   end
   def #{attribute}()
    @#{attribute}
   end
  end
 "
end
 
add_checked_attribute(String, :my_attr)
t = "hello,kitty"
 
t.my_attr = 100
puts t.my_attr
 
t.my_attr = false
puts t.my_attr

In this step, the eval method is used to open the class with the class and def keywords respectively, and the get and set methods of the specified attribute are defined. The set method will simply judge whether the value is empty (nil or false). If so, an invalid attribute exception will be thrown.

Setp 2

?
1
2
3
4
5
6
7
8
9
10
11
12
13
def add_checked_attribute(klass, attribute)
 klass.class_eval do
  define_method "#{attribute}=" do |value|
   raise "Invaild attribute" unless value
   instance_variable_set("@#{attribute}", value)
  end
 
  define_method attribute do
   instance_variable_get "@#{attribute}"
  end
 
 end
end

This step replaces the eval method and uses class_ Eval and define_ The method method replaces the previous class and def keywords, and instance is used for setting and obtaining instance variables_ variable_ Set and instance_ variable_ The get method is no different from the first step in use, but there are some internal implementation differences.

Step 3

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def add_checked_attribute(klass, attribute, &validation)
 klass.class_eval do
  define_method "#{attribute}=" do |value|
   raise "Invaild attribute" unless validation.call(value)
   instance_variable_set("@#{attribute}", value)
  end
 
  define_method attribute do
   instance_variable_get "@#{attribute}"
  end
 
 end
end
 
add_checked_attribute(String, :my_attr){|v| v >= 180 }
t = "hello,kitty"
 
t.my_attr = 100 #Invaild attribute (RuntimeError)
puts t.my_attr
 
t.my_attr = 200
puts t.my_attr #200

It’s nothing strange. It just adds code block verification, which increases the flexibility of verification and is no longer limited to nil and false.

Step 4

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Class
 def attr_checked(attribute, &validation)
   define_method "#{attribute}=" do |value|
    raise "Invaild attribute" unless validation.call(value)
    instance_variable_set("@#{attribute}", value)
   end
 
   define_method attribute do
    instance_variable_get "@#{attribute}"
   end
 end
end
 
String.add_checked(:my_attr){|v| v >= 180 }
t = "hello,kitty"
 
t.my_attr = 100 #Invaild attribute (RuntimeError)
puts t.my_attr
 
t.my_attr = 200
puts t.my_attr #200

Here, we put the method name in the previous top-level scope into class. Since all objects are instances of class, the instance method defined here can also be accessed by all other classes in ruby. At the same time, in the class definition, self is the current class, so the parameter and class of calling class are omitted_ Eval method, and we changed the name of the method to attr_ checked。

Step 5

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
module CheckedAttributes
 def self.included(base)
  base.extend ClassMethods
 end
end
 
module ClassMethods
 def attr_checked(attribute, &validation)
   define_method "#{attribute}=" do |value|
    raise "Invaild attribute" unless validation.call(value)
    instance_variable_set("@#{attribute}", value)
   end
 
   define_method attribute do
    instance_variable_get "@#{attribute}"
   end
 end
end
 
class Person
 include CheckedAttributes
 
 attr_checked :age do |v|
  v >= 18
 end
end

In the last step, after the checkedattributes module is introduced, the current class is extended through the introduced module through the hook method, so that the current class supports the method calls after the introduction, that is, the get and set method groups here.

At this point, we have got a file called attr_ Checked, similar to attr_ Accessor class macro, through which you can verify the properties you want.