Ruby 的类宏
在我们编写 Ruby 的代码时,经常会见到一些这样像关键字的方法例如:attr_accessor
,这种方法我们称它为类宏(Class Macros),类宏是仅在定义类时使用的类方法。它们使我们可以跨类的共享代码。本章节让我们来深入了解一下它。
1. 创建一个类宏
让我们在attr_accessor
的基础上来做一个新的类宏attr_checked
,这个类宏可以为一个类赋予属性,包括getter
与setter
方法,并可以通过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 方法
让我们从定义一个标准的age
的getter
和setter
方法开始。
实例:
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
以及实例变量set
和get
方法来实现这个方法。
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. 小结
在本章节中,我们一步一步创建一了个类宏。宏在今后的开发中会为您省去大量的时间,大量降低维护成本和沟通成本。