Ruby中钩子方法的运用实例解析

    通过使用钩子方法,可以让我们在Ruby的类或模块的生命周期中进行干预,可以极大的提高编程的灵活性。
    与生命周期相关的钩子方法有下面这些:

    类与模块相关

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

    单件类相关

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

    示例代码

    
    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
    
    # 输出
    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
    
    # 输出
    New method: M#my_method
    
    

    除了上面列出来的一些方法外,也可以通过重写父类的某个方法,进行一些过滤操作后,再通过调用super方法完成原函数的功能,从而实现类似钩子方法的功效,如出一辙,环绕别名也可以作为一种钩子方法的替代实现。

    运用实例
    任务描述:

    写一个操作方法类似attr_accessor的attr_checked的类宏,该类宏用来对属性值做检验,使用方法如下:

    
    class Person
     include CheckedAttributes
    
     attr_checked :age do |v|
      v >= 18
     end
    end
    
    me = Person.new
    me.age = 39 #ok
    me.age = 12 #抛出异常
    
    

    实施计划:

    使用eval方法编写一个名为add_checked_attribute的内核方法,为指定类添加经过简单校验的属性
    重构add_checked_attribute方法,去掉eval方法,改用其它手段实现
    添加代码块校验功能
    修改add_checked_attribute为要求的attr_checked,并使其对所有类都可用
    通过引入模块的方式,只对引入该功能模块的类添加attr_checked方法
    Step 1

    
    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
    
    

    这一步使用eval方法,用class和def关键词分别打开类,且定义了指定的属性的get和set方法,其中的set方法会简单的判断值是否为空(nil 或 false),如果是则抛出Invalid attribute异常。

    Setp 2

    
    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
    
    

    这一步更换掉了eval方法,同时也分别用class_eval和define_method方法替换了之前的class与def关键字,实例变量的设置和获取分别改用了instance_variable_set和instance_variable_get方法,使用上与第一步没有任何区别,只是一些内部实现的差异。

    Step 3

    
    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
    
    

    没有什么奇特的,只是加了通过代码块验证,增加了校验的灵活性,不再仅仅局限于nil和false之间了。

    Step 4

    
    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
    
    

    这里我们把之前顶级作用域中方法名放到了Class中,由于所有对象都是Class的实例, 所以这里定义的实例方法,也能被Ruby中的其它所有类访问到,同时在class定义中,self就是当前类,所以也就省去了调用类这个参数和class_eval方法,并且我们把方法的名字也改成了attr_checked。

    Step 5

    
    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
    
    

    最后一步通过钩子方法,在CheckedAttributes模块被引入后,对当前类通过被引入模块进行扩展, 从而使当前类支持引入后的方法调用,即这里的get与set方法组。

    到此,我们已经得到了一个名为attr_checked,类似attr_accessor的类宏,通过它你可以对属性进行你想要的校验。