如何理解 Ruby 类的实例变量

最近在项目中需要定义不同的规则,由于规则是可能经常变化,且规则数量可能较多。于是用了一点 Ruby 的元编程特性,来实现一种简单的 DSL 从而来简化整个工程的结构。先来看看这个简单的 DSL 的用法来理解下我们的需求:

# 假设定义一个 Validator 用来检查用户密码是否合规
class PasswordValidator < RulesValidator 
    # 定义一条检查密码长度的规则 
    rule "rule1", "密码数据不能为空" do |data| 
        raise "密码数据不能为空" if (data.nil?) 
    end 

    # 定义一条检查密码长度的规则 
    rule "rule2", "检查密码长度是否大于等于8" do |data| 
        raise "密码长度至少为8位" unless data.length >= 8
    end
end

# 假设定义一个 Validator 用来检查用户密码是否合规
class UsernameValidator < RulesValidator
    # 定义一条检查密码长度的规则
    rule "rule3", "用户名不能为空" do |data|
        raise "用户名不能为空" if (data.nil?)
    end
end

这样,我们可以将不同的规则实现到不同的 class 中,熟悉 Sinatra 的同学会发现,这种使用方式和 Sinatra 中使用 post/get 等关键字定义 REST 服务的方式非常类似 。现在的问题是,如何实现 RulesValidator 以及如何使用 PasswordValidator 和 UsernameValidator 来取进行校验。在上述示例代码中,DSL 关键字 ‘rule’ 所支持的逻辑,实际上是在类定义中实现的,有点类似于用 def 关键字定义一个类的方法。但实际上,’rule’ 是 Ruby 解析器在解析类定义时所执行的一个函数。因此 ‘rule’ 必须实现成类方法(可以利用 class << self ~ end 方式来定义),从而让解析器在解析子类时能够进行调用。从这两个类的使用方式来看,有两种实现方式,但实际的效果在 Ruby 中其实有很大差别。

1. 每次创建 RulesValidator 子类的实例进行校验,例如:

password_validator = PasswordValidator.new 
password_validator.validate("password")

username_validator = UsernameValidator.new
username_validator.validate("username")

对于这种方式,采用最直接的相关的 RulesValidator 实现如下:

class RulesValidator
    # 定义类方法 (class method),可以直接通过类名进行调用
    class << self
        # 定义一条规则,规则的检查逻辑通过 block 来执行
        # @@rules ||= [] 为初始化 @rules 对象为一个数组
        def rule(name, desc, &rule_block)
            (@@rules ||= []) << { :name => name, 
                :desc => desc, 
                :rule_block => rule_block }
        end 
    
    end
    
    # 调用添加的所有规则来对数据进行检查
    def validate(data)
        @@rules.each do |rule|
            rule[:rule_block].call(data)
        end
    end
    
    def print_rules
        puts @@rules
    end
end

在这种实现方式中,由于要创建类实例来进行调用,’validate’ 只是普通的方法而非类方法(class method),因此仅在创建的类实例上可以进行调用。’rule’ 实现为类方法,并且很自然地使用了类变量 @@rule 将动态创建的规则存放在其中,从而保证规则在子类解析时可以创建。但是我们在测试时会发现,我们只对其中的一个 Validator 进行调用,就输出了所有定义的所有三个规则内容:

validator = PasswordValidator.new
validator.print_rules

Output:

{:name=>"rule1", :desc=>"\u5BC6\u7801\u6570\u636E\u4E0D\u80FD\u4E3A\u7A7A", :rule_block=>#<proc:0x007fbf20177460@ users="" dreamleft="" sandbox="" ruby="" rules.rb:32="">}
{:name=>"rule2", :desc=>"\u68C0\u67E5\u5BC6\u7801\u957F\u5EA6\u662F\u5426\u5927\u4E8E\u7B49\u4E8E8", :rule_block=>#<proc:0x007fbf20177398@ users="" youjing="" sandbox="" ruby="" rules.rb:37="">}
{:name=>"rule3", :desc=>"\u7528\u6237\u540D\u4E0D\u80FD\u4E3A\u7A7A", :rule_block=>#<proc:0x007fbf20177258@ users="" dreamleft="" sandbox="" ruby="" rules.rb:45="">}
</proc:0x007fbf20177258@></proc:0x007fbf20177398@></proc:0x007fbf20177460@>

很明显,@@rules 作为 class variable,在所有子类中共享了父类的 class variable,这不符合我们的要求,PasswordValidator 应当只包含其自身所定义的两个规则才对。

2. 直接使用 RulesValidator 子类的类方法进行校验,例如:

PasswordValidator.validate("password")
UsernameValidator.validate("username")

其对应的实现代码如下:

class RulesValidator
    # 定义类方法 (class method),可以直接通过类名进行调用
    class << self
        # 定义一条规则,规则的检查逻辑通过 block 来执行
        # @rules ||= [] 为初始化 @rules 对象为一个数组
        def rule(name, desc, &rule_block)
            (@rules ||= []) << {
                :name => name, 
                :desc => desc, 
                :rule_block => rule_block }
        end 

        # 调用添加的所有规则来对数据进行检查
        def validate(data)
            @rules.each do |rule|
                rule[:rule_block].call(data)
            end
        end

        def print_rules
            puts @rules
        end
    end
    
end

在这个过程中,使用了类方法(class method)来添加新的规则,而新的规则被存放到 @rule 实例变量 (instance variable) 中。此外,’validate’ 也被实现成了 class method。让我们再来看看 PasswordValidator 有几条规则:

PasswordValidator.print_rules

Output:

{:name=>"rule1", :desc=>"\u5BC6\u7801\u6570\u636E\u4E0D\u80FD\u4E3A\u7A7A", :rule_block=>#<proc:0x007fbf20177460@ users="" dreamleft="" sandbox="" ruby="" rules.rb:32="">}
{:name=>"rule2", :desc=>"\u68C0\u67E5\u5BC6\u7801\u957F\u5EA6\u662F\u5426\u5927\u4E8E\u7B49\u4E8E8", :rule_block=>#<proc:0x007fbf20177398@ users="" youjing="" sandbox="" ruby="" rules.rb:37="">}
</proc:0x007fbf20177398@></proc:0x007fbf20177460@>

这时可以发现, PasswordValidator 中正确地输出了两条规则。这是由于用于存放 @rule 是实例变量 (instance variable) 分别由两个子类分别持有。那么问题来了,既然 @rule 是实例变量,那么能否将 ‘validate’ 方法移出 class << self ~ end,使其变成一个普通的方法,从而可以对子类进行实例化之后再进行调用呢?例如:

class RulesValidator
    # 定义类方法 (class method),可以直接通过类名进行调用
    class << self
        # 定义一条规则,规则的检查逻辑通过 block 来执行
        # @rules ||= [] 为初始化 @rules 对象为一个数组
        def rule(name, desc, &rule_block)
            (@rules ||= []) << {
                :name => name, 
                :desc => desc, 
                :rule_block => rule_block }
        end 

    end

    # 普通方法
    # 调用添加的所有规则来对数据进行检查
    def validate(data)
        @rules.each do |rule|
            rule[:rule_block].call(data)
        end
    end

    def print_rules
        puts @rules
    end
    
end

validator = PasswordValidator.new
validator.print_rules
validator.validate("abc")

Output:

rules.rb:17:in `validate': undefined method `each' for nil:NilClass (NoMethodError)
        from rules.rb:55:in `

我们很奇怪的发现出错了,print_rules 方法根本没有打印出任何东西,而且在 ‘validate’ 方法调用时出错了,说明 @rule 时 nil,根本没有被初始化。为什么会这样呢?不是所有的 Ruby 公开文档都说 @variable 是实例变量,应该是属于类实例的吗?好吧,让我们来揭开谜底,实际上,在上一段代码中,虽然名称相同,但是’validate’ 中的 @rules 和 ‘rule’ @rules 根本就是两个不同的对象。在 Ruby 世界中,所有东西都是对象 (object),其实不存在类似于 Java/C++ 这种强类型语言的类成员。@variable 就应该理解为对象的成员,当 @variable 的 scope 在类方法中 (class method)时,它就属于类对象(class object)本身;当 @variable 在普通的方法中声明时,它就属于类实例化后的对象 (object)。

下面给出一段代码,可以更清楚地看清楚 Ruby 中各种不同 variable 的使用范围:

class TestVariables

    @class_instance_variable1 = "1"
    @class_instance_variable2 = "2"
    @@class_variable_1 = "1"
    @@class_variable_2 = "2"

    # variable2 = "2"

    def initialize
        @instance_variable1 = "1"
        @instance_variable2 = "2"
    end

    class << self
        def class_method
            @rules = []
        end
    end
end

class TestVariablesChild < TestVariables
end


object = TestVariables.new
puts "Class variables: #{TestVariables.class_variables}"
puts "Instance variables: #{object.instance_variables}"
puts "Instance variables of the TestVariables Class: #{TestVariables.instance_variables}"
TestVariables.class_method
puts "----"
puts "Instance variables: #{object.instance_variables}"
puts "Instance variables of the TestVariables Class: #{TestVariables.instance_variables}"
puts "----"
TestVariablesChild.class_method
puts "Instance variables of the TestVariablesChild Class: #{TestVariablesChild.instance_variables}"

输出结果如下:

Class variables: [:@@class_variable_1, :@@class_variable_2]
Instance variables: [:@instance_variable1, :@instance_variable2]
Instance variables of the Class: [:@class_instance_variable1, :@class_instance_variable2]
----
Instance variables: [:@instance_variable1, :@instance_variable2]
Instance variables of the Class: [:@class_instance_variable1, :@class_instance_variable2, :@rules]
----
Instance variables of the Class: [:@rules]

可以看到,父类和子类分别拥有自己的 instance variable,我姑且称之为 class instance variable 用来表示这种变量是创建在类对象上的。并且值得注意的是,子类并未直接继承父类的实例变量。对父类子类分别调用 class_method 初始化 @rule 变量,分别创建出两个不同的 @rule 变量。

通过这冗长的描述,可以大致看到 Ruby 编程(特别是元编程)的神奇和灵活之处,语言本身就像橡皮泥,可以随意捏成你想要的样子。但这也像是一把双刃剑,如果不细致观察,仔细体会其中的细微差别,就有可能“失之毫厘,谬以千里”。所以,编程语言之间还是存在诸多的风格差异,还是比较习惯 Java 这种强类型语言一是一、二是二的感觉,只可惜在互联网处处讲究快速部署的时代,Java 语言的各种生态确有些太重了。

1,918 次阅读

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注