任务:写一个类宏
这个类宏与 attr_accessor 类似,名字叫 attr_checked 方法,它有如下特征:
- 它会创建经过检验的属性
- 接受属性名和代码块,代码块用来校验,如果对一个属性赋值,而代码块没有返回
true,就会抛出异常。 - attr_checked并不在每个类中都可用,因为它的初衷并不是把标准库搞的乱七八糟,只有当一个类包含CheckedAttributes模块时才能使用这个方法。
开发步骤:
使用 eval() 编写一个名为 add_checked_attribute() 的内核方法,为类添加一个最简单的经过校验的属性,比如说 age 。
这里用 eval 方法(背景知识2,3,4)是为了快速通过测试。
|
|
这里的 add_checked_attribute 方法为什么要仿照 attr_accessor 的创建的读写拟态方法呢?也许因为方法的核心内容就是读写吧。在明确了读写对象的基础上才有了比较等其它操作。所以第一步的核心是先开发出读写方法。
重构 add_checked_attribute() 方法,去掉 eval() 。
因为 eval 的缺点(背景知识3),接下来去掉 eval() 。
不用 eval() 方法,就无法使用代码字符串,继续使用 class 关键字,因为 class 不接受变量作为类名,
#{klass}和的值无法动态传入。此时,可以用 class_eval 方法进入类的作用域。同时在定义方法时也不能用 def 了,因为只有在运行时才知道方法的名字。这里可以用
defind_method来动态定义方法。至于操作实例变量,可以用 instance_variable_get 和 instance_variable_set 方法。1234567891011121314151617181920212223242526272829303132333435363738require 'test/unit'class Person; endclass TestCheckedAttribute < Test::Unit::TestCasedef setupadd_checked_attribute(Person, :age)@bob = Person.newenddef test_accepts_valid_values@bob.age = 20assert_equal 20, @bob.ageenddef test_refuses_nil_valuesassert_raises RuntimeError, 'Invalid attribute' do@bob.age = nilendenddef test_refuses_false_valuesassert_raises RuntimeError, 'Invalid attribute' do@bob.age = falseendendenddef add_checked_attribute(klass, attribute)klass.class_eval dodefine_method "#{attribute}=" do |value|raise 'Invalid attribute' unless valueinstance_variable_set("@#{attribute}", value)enddefine_method attribute doinstance_variable_get "@#{attribute}"endendend通过块来校验属性。
目前代码只能简单地对赋值 nil 或 false 的情况抛出异常,没有代码块验证的灵活。这里通过 &validation 来做代码块验证。
1234567891011121314151617181920212223242526272829303132require 'test/unit'class Person; endclass TestCheckedAttribute < Test::Unit::TestCasedef setupadd_checked_attribute(Person, :age) {|v| v >= 18 }@bob = Person.newenddef test_accepts_valid_values@bob.age = 20assert_equal 20, @bob.ageenddef test_refuses_invalid_valuesassert_raises RuntimeError, 'Invalid attribute' do@bob.age = 17endendenddef add_checked_attribute(clazz, attribute, &validation)clazz.class_eval dodefine_method "#{attribute}=" do |value|raise 'Invalid attribute' unless validation.call(value)instance_variable_set("@#{attribute}", value)enddefine_method attribute doinstance_variable_get "@#{attribute}"endendend将 add_checked_attribute() 改名为 attr_checked 的类宏,它对所有类可用。
这意味着应该定义一个可以在类定义中使用的方法,另外新方法不能像 add_checked_attribute() 一样接受类名作为参数,应该只接受属性名作为参数。
1234567891011121314151617181920212223242526272829303132333435require 'test/unit'class Personattr_checked: age do |v|v >= 18endendclass TestCheckedAttribute < Test::Unit::TestCasedef setup@bob = Person.newenddef test_accepts_valid_values@bob.age = 20assert_equal 20, @bob.ageenddef test_refuses_invalid_valuesassert_raises RuntimeError, 'Invalid attribute' do@bob.age = 17endendendclass Classdef attr_checked(attribute, &validation)define_method "#{attribute}=" do |value|raise 'Invalid attribute' unless validation.call(value)instance_variable_set("@#{attribute}", value)enddefine_method attribute doinstance_variable_get "@#{attribute}"endendend写一个模块,通过钩子方法(背景知识5)为指定的类添加 attr_checked 方法。
12345678910111213141516171819202122232425262728293031323334353637module CheckedAttributesdef self.included(base)base.extend ClassMethodsendmodule ClassMethodsdef attr_checked(attribute, &validation)define_method "#{attribute}=" do |value|raise 'Invalid attribute' unless validation.call(value)instance_variable_set("@#{attribute}", value)enddefine_method attribute doinstance_variable_get "@#{attribute}"endendendendrequire 'test/unit'class Personinclude CheckedAttributesattr_checked :age do |v|v >= 18endendclass TestCheckedAttributes < Test::Unit::TestCasedef setup@bob = Person.newenddef test_accepts_valid_values@bob.age = 18assert_equal 18, @bob.ageendend
最后的结果是,公用验证代码放在 CheckedAttributes 模块中,类宏 attr_checked 验证条件在类中使用,测试液运行正常。代码结构干净利索。
背景知识补充:
1.绑定对象
Binding 就是一个用对象表示的完整作用域。可以通过 Binding 对象和来捕获并带走当前作用域,然后使用 eval 来计算结果,实现类似于闭包的操作。
Binding.pry会在当前绑定上打开一个Ruby解释器。
|
|
2.eval 的用处
eval 方法自带共享作用域(和变量)的功能,能够自动衔接目标类的上下文。
从外部传入一个任意的代码字符串给 eval 方法,这样就可以创建一个简单的Ruby解释器。很多Ruby解释器都使用了 eval 方法。
3.eval 的麻烦
1.不支持编辑器的功能特性,比如语法高亮和自动完成。
2.难以阅读和修改
3.Ruby执行字符串前不会进行语法检查
4.安全性不好——有可能会被代码注入
4.防止代码注入:
1.限制 eval 方法只执行自己写的代码字符串。
2.禁用 eval 方法。
3.识别污染对象和设置安全级别。
5.钩子方法
Ruby 提供的钩子方法种类繁多,覆盖了对象模型中绝大多数事件。当中这些事件发生(如被包含,扩展,方法增加删除等),就会触发它们。
通过覆写钩子方法如 Module#included() 方法,使得当一个模块被包含时执行额外的代码。它跟环绕别名所起到的作用有点像。
就算不覆写,也可以借助环绕别名把普通方法变成钩子方法。
尾声
用平常心对待代码,它应该简洁而清晰,而不是晦涩难懂。
要有智慧忘掉所学的东西,根本没有什么元编程,只有编程而已。