最近在项目中需要定义不同的规则,由于规则是可能经常变化,且规则数量可能较多。于是用了一点 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 语言的各种生态确有些太重了。