新手问题 元编程中验证的问题

engin123456789 · 2014年01月23日 · 最后由 engin123456789 回复于 2014年01月26日 · 2872 次阅读

《ruby 元编程》中有这样一个例子: 写一个类似类宏 attr_accessor 的用来检验参数是否合法的方法。 但是使用中似乎有一中情况会被绕过:

模块代码:

module CheckAttr
    def self.included base
        base.extend ClassModule
    end

    module ClassModule
        def attr_checked attr, &validate_block
            define_method "#{attr}=" do |value|
                raise 'invalidate attribuate' unless validate_block.call value
                instance_variable_set "@#{attr}",value
            end

            define_method attr do
                instance_variable_get "@#{attr}"
            end
        end
    end
end

使用代码:

class Person
    include CheckAttr
    attr_checked :age do |v|
        v >= 18
    end
    def initialize name,age
        @name = name
        @age = age
    end
end

这种情况下如果直接使用person = Person.new 'shiyj',16就会绕过这种检验,而只有使用person.age = 16才会执行。

那么有没有一种更好的实现,使这个·attr_checked·能够像·attr_accessor·一样既定义@age,又可以在@age=时来控制??

不知道有没有@age=的时候能触发的 hook,不过根据你的需求可以实现在 initialize 的时候验证

module CheckAttr
    def self.included base
        base.extend ClassModule

        base.class_eval do 
            def self.method_added method_name
                if method_name == :initialize && !@_mark_
                    class_eval do
                        alias_method :original_initialize, :initialize
                        @_mark_ = true

                        def initialize *args, &blk
                            original_initialize *args, &blk
                            instance_variables.each do |v| 
                               method_name = :"#{v.to_s.gsub('@', '')}="
                                __send__(method_name, instance_variable_get(v)) if respond_to? method_name
                            end
                        end                        
                    end
                end
            end

        end

    end

#1 楼 @piecehealth 太复杂。。。

构造函数换个方式写

class Person
    include CheckAttr
    attr_checked :age do |v|
        v >= 18
    end
    def initialize name,age
        @name = name
        self.age = age
    end
end

#1 楼 @piecehealth 这个方法确实有点复杂,并且如果在其他的方法内也用到了经过计算后的数值的赋值的话还是会绕开,每个方法都加严重又有点太过了。 貌似可以将@ageCheckAttr定义为一个白板类的实例,这样在赋值时就可以验证。只是还不太理解 ruby 的继承实现……

#2 楼 @davidqhr 这个模块会是被别人使用的 gem,这个不能保证用户的赋值按照规定吧……

#4 楼 @engin123456789 @age 是封装在类里面的,外面不能直接访问和赋值

#3 楼 @engin123456789 每个方法都加一遍验证一点都不过,我上面的代码稍微改动一下就行了,用 method_added 的钩子,自动 hack 每一个新加的方法,完成一遍验证。

#3 楼 @engin123456789 顺手写了一下: 完整的 CheckAttr Module:

module CheckAttr
    def self.included base
        base.extend ClassModule
        base.class_eval do
            def self.method_added method_name
                if @_last_modified_method != method_name && method_name.to_s !~ /^_check_attr_/ && method_name.to_s !~ /=$/
                    @_last_modified_method = method_name
                    class_eval do
                        alias_method :"_check_attr_hacked_#{method_name}", method_name
                        define_method method_name do |*args, &blk|
                            ret = __send__(:"_check_attr_hacked_#{method_name}", *args, &blk)
                            instance_variables.each do |v| 
                               method_name = :"#{v.to_s.gsub('@', '')}="
                                __send__(method_name, instance_variable_get(v)) if respond_to? method_name
                            end
                            return ret
                        end
                    end
                end
            end
        end
    end

    class InvalidAttributeError < Exception; end

    module ClassModule
        def attr_checked attr, &validate_block
            define_method "#{attr}=" do |value|
                raise InvalidAttributeError.new("@#{attr}") unless validate_block.call value
                instance_variable_set "@#{attr}",value
            end

            define_method attr do
                instance_variable_get "@#{attr}"
            end
        end
    end


end

Usage:

class Person
    include CheckAttr
    attr_checked :age do |v|
        v >= 18
    end
    attr_checked(:weight) {|v| v >= 40}
    def initialize name,age, weight
        @name = name
        @age = age
        @weight = weight
    end

    def lose_weight w
        @weight -= w
    end
end

#person1 = Person.new 'shiyj',16, 50 # raise: InvalidAttributeError: @age
person2 = Person.new 'Fat Guy', 35, 200
person2.lose_weight 180 # raise InvalidAttributeError: @weight

大体就这思路,你可以再调整一下。 又或者你改一下你的思路,做成显示的验证,比如在执行 save 一类的时候验证,会方便高效许多。

#7 楼 @piecehealth 侵入性太高了。

#6 楼 @piecehealth 我理解了,class_eval 之后就完全将@age=替换为self.age=。我之前还以为每次调用方法都需要重新改变一次。谢谢^_^……

需要 登录 后方可回复, 如果你还没有账号请 注册新账号