跳到主要内容

Ruby 的类宏

在我们编写 Ruby 的代码时,经常会见到一些这样像关键字的方法例如:attr_accessor,这种方法我们称它为类宏(Class Macros),类宏是仅在定义类时使用的类方法。它们使我们可以跨类的共享代码。本章节让我们来深入了解一下它。

1. 创建一个类宏

让我们在attr_accessor的基础上来做一个新的类宏attr_checked,这个类宏可以为一个类赋予属性,包括gettersetter方法,并可以通过Block来对属性的值进行校验。

具体表现形式为:

class Person
include CheckedAttributes

attr\_checked(:age){|age| age >= 18}
attr\_checked(:sex){|sex| sex == 'man'}
# ...

end

me = Person.new
me.age = 25
me.man = 'man'
puts me.age
puts me.man


# ---- 正常情况下的预期结果 ----
25
man

而当我们不能通过校验的时候。

other = Person.new
other.age = 17 # 预期结果:抛出异常
other.sex = 'woman' # 预期结果:抛出异常

1.1 getter 和 setter 方法

让我们从定义一个标准的agegettersetter方法开始。

实例:

class Person
def age= age
@age = age
end

def age
@age
end
end

me = Person.new
me.age = 18
puts me.age

# ---- 输出结果 ----
18

1.2 使用eval来运行多行代码字符串

让我们把定义class Person这部分当做多行字符串,使用eval来执行。

eval %Q{ 
class Person
def age= age
@age = age
end

def age
@age
end
end
}

me = Person.new
me.age = 18
puts me.age

# ---- 输出结果 ----
18

1.3 动态赋予类属性

类的补丁章节我们知道了重复定义类并不会创建同名的类,只会在其基础上增加实例方法或类方法。所以我将刚刚定义类的字符串封装成方法来为Person类添加属性。

def add\_checked\_attribute(klass, attribute)
eval %Q{
class #{klass}
def #{attribute}= #{attribute}
@#{attribute}= #{attribute}
end

def #{attribute}
@#{attribute}
end
end
}
end

add\_checked\_attribute(:Person, :age)
add\_checked\_attribute(:Person, :sex)


me = Person.new
me.age = 18
me.sex = 'man'
puts me.age
puts me.sex

# ---- 输出结果 ----
18
man

1.4 去掉eval,重构方法

使用eval有时候并不是一个好办法,会影响整体代码的可读性和维护性,因此我们使用class_eval以及实例变量setget方法来实现这个方法。

class Person
end

def add\_checked\_attribute(klass, attribute)
klass.class_eval do
define\_method "#{attribute}=" do |value|
instance\_variable\_set("@#{attribute}", value)
end

define\_method attribute do
instance_variable_get "@#{attribute}"
end
end
end

add\_checked\_attribute(Person, :age)
add\_checked\_attribute(Person, :sex)


me = Person.new
me.age = 18
me.sex = 'man'
puts me.age
puts me.sex

# ---- 输出结果 ----
18
man

注意事项:这时因为我们现在不定义Person类,所以需要在最前面先定义一个Person类,否则Ruby会因为无法找到Person类而报错。

1.5 增加校验属性的Block

让方法对传入的Block值进行校验

class Person
end

def add\_checked\_attribute(klass, attribute, &validation)
klass.class_eval do
define\_method "#{attribute}=" do |value|
raise 'Invalid 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(Person, :age) {|age| age >= 18}
add\_checked\_attribute(Person, :sex) {|age| age == 'man'}


me = Person.new
me.age = 18
me.sex = 'man'
puts me.age
puts me.sex

# ---- 输出结果 ----
18
man

当我们赋予属性的值不满足条件的时候会抛出异常。

me = Person.new
me.sex = 'woman'

# ---- 输出结果 ----
Invalid attribute! (RuntimeError)

1.6 最后将方法定义到模块,完成类宏

我们在引入类宏的模块的时候使用的是include,所以我们使用included钩子方法,在钩子方法对引用的类进行extend(因为extend模块添加类方法),替代之前的class_eval,将之前定义属性的方法定义到被extend的模块中,从而使定义的方法可以被类调用(类方法)。

# 定义模块部分
module CheckedAttributes
def self.included(klass)
klass.extend ClassMethods
end
end

module ClassMethods
def attr\_checked(attribute, &validation)
define\_method "#{attribute}=" do |value|
raise 'Invalid 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 {|age| age >= 18}
attr_checked :sex {|sex| sex == 'man'}
end

me = Person.new
me.age = 18
me.sex = 'man'
puts me.age
puts me.sex

# ---- 输出结果 ----
18
man

当我们赋予属性的值不满足条件的时候同样会抛出异常。

me = Person.new
me.age = 10

# ---- 输出结果 ----
Invalid attribute! (RuntimeError)

2. 小结

在本章节中,我们一步一步创建一了个类宏。宏在今后的开发中会为您省去大量的时间,大量降低维护成本和沟通成本。