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. 小结

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