3-16 提取任务(第6章)

The boss's mission:

写一个类宏,功能与attr_accessor类似,但会创建经过校验的属性,名字attr_checked。

需求:

  1. 接受属性名,和block。block用于校验属性,如果对一个属性赋值,非true就报错。
  2. 只给特定的类用,所以不要放到标准库中。只有当类加了CheckedAttributes模块,才拥有这个功能。

A Development Plan:

开发计划:

  1. 使用eval方法快速编写内核方法add_checked_attribute,用来为类添加一个校验属性。
  2. 重构这个方法,不用eval.
  3.  通过代码块来校验属性。
  4. 把这个方法修改为名为attr_checked的类宏,让它对所有类可用。
  5. 写一个模块,通过hook method为指定的类添加attr_checked方法。

第一步 

创建2个拟态方法,读/写 方法。在写方法加入校验属性值是否nil/false.

require 'test/unit'
class Person; end
class TestCheckedAttribute < Test::Unit::TestCase
  def setup
    add_checked_attribute(Person, :age)
    @bob = Person.new
  end
  def test_accepts_valid_values
    @bob.age = 20
    assert_equal 20, @bob.age
  end
  def test_refuses_nil_values
    assert_raises RuntimeError, 'Invalid attribute' do
      @bob.age = nil
    end
  end
  def test_refuses_false_values
    assert_raises RuntimeError, 'Invalid attribute' do
      @bob.age = false
    end
  end
end
def add_checked_attribute(klass, attribute)
  eval "
    class #{klass}
      def #{attribute}=(value)
        raise 'Invalid attribute' unless value
        @#{attribute} = value
      end
      def #{attribute}()
        @#{attribute}
      end
    end
  "
end
add_checked_attribute(String, :my_attr)

第二步 ,重构

 防止代码外泄后,被攻击,另外增强代码可读性,所以不用eval。标准库里寻找替代方法,注意scope。使用class_eval重新定义类。

def add_checked_attribute(klass, attribute)
  klass.class_eval do
    ...
  end
end

定义读写方法,不能用def关键字,改用动态方法传递参数。

另外,不能使用"@#{attribute}" =  value 这种字符串给实例变量赋值了。改用其他method,Object#instance_variable_set().

结果:

def add_checked_attribute(klass, attribute)
  klass.class_eval do
    define_method("#{attribute}=") do |value|
      raise 'Invalid attribute' unless value
      instance_variable_set("@#{attribute}", value)
    end
    define_method attribute do
      instance_variable_get("@#{attribute}")
    end
  end
end

 第三步

 增加block验证,测试加入代码块条件,{|v| v >= 18 }

 定义add_checked_attribute方法增加Proc参数, &validation

 在raise上修改为Proc.call(value) ,Invokes the block.

class TestCheckedAttribute < Test::Unit::TestCase
  def setup
    add_checked_attribute(Person, :age) {|v| v >= 18 }
    @bob = Person.new
  end
...
  def test_refuses_invalid_values
    assert_raises RuntimeError, 'Invalid attribute' do
      @bob.age = 17
    end
  end
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
...

 第四步,校验过的属性。

 把内核方法add_checked_attribute改造成类宏attr_checked,放到类Person中。 

class Person
  attr_checked :age do |v|
    v >= 18
  end
end

如果让 attr_checked 对所以方法可用,可以定义为Class或Module的实例方法。

Person的类是Class。Person作为Class的对象可以调用attr_checked方法 .

这样就不需要class_eval打开类了。因为attr_checked执行时要定义的类self就是Person. 

class Class
  def add_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

同时,要修改测试类的代码,去掉  add_checked_attribute(Person, :age) {|v| v >= 18 } 


 

第 5 步 hook methods

定义一个模块,包含include到Person中。 

  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

但是, 我们的目的是做出atrr_checked,这是是类宏,类方法。

而类包含的方法都是实例方法,Person.attr_checked是❌的。

所以想办法让attr_checked成为Person的类方法。

这里使用Hook methods. Object#included.

  1. 当Person包含模块CheckAttributes时,会自动调用钩子方法included.
  2. 这个钩子方法传递参数就是类的名字,使用extend方法进行类扩展
  3. extend方法把模块ClassMethods中的方法attr_checked包含到Person的单件类中。
所以,attr_checked就成了类方法,可以被类直接使用了,模拟了类宏。

⚠️ :这里attr_checked内生成的动态方法,是类Person的实例方法。

类方法的定义 有三种。分别是

  • def 类名.method;end
  • 在类中,用def self.method;end
  • 在类中,用class << self...end

 代码:

module CheckedAttributes
  def self.included(base)
    base.extend ClassMethods
  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
end

 小结 :Wrap_up

这是一个有难度的元编程问题,编写了自己定义的类宏。

最终的全代码 

require 'test/unit'
class TestCheckedAttribute < Test::Unit::TestCase
  def setup
    @bob = Person.new
  end
  def test_accepts_valid_values
    @bob.age = 20
    assert_equal 20, @bob.age
  end
  def test_refuses_invalid_values
    assert_raises RuntimeError, 'Invalid attribute' do
      @bob.age = 17
    end
  end
end
module CheckedAttributes
  def self.included(base)
    base.extend ClassMethods
  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
end
class Person
  include CheckedAttributes
  self.attr_checked( :age ) do |v|
    v >= 18
  end
end
原文地址:https://www.cnblogs.com/chentianwei/p/8583607.html