Ruby 的類宏
在我們編寫 Ruby 的代碼時(shí),經(jīng)常會見到一些這樣像關(guān)鍵字的方法例如:attr_accessor
,這種方法我們稱它為類宏(Class Macros),類宏是僅在定義類時(shí)使用的類方法。它們使我們可以跨類的共享代碼。本章節(jié)讓我們來深入了解一下它。
1. 創(chuàng)建一個(gè)類宏
讓我們在attr_accessor
的基礎(chǔ)上來做一個(gè)新的類宏attr_checked
,這個(gè)類宏可以為一個(gè)類賦予屬性,包括getter
與setter
方法,并可以通過Block
來對屬性的值進(jìn)行校驗(yàn)。
具體表現(xiàn)形式為:
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
# ---- 正常情況下的預(yù)期結(jié)果 ----
25
man
而當(dāng)我們不能通過校驗(yàn)的時(shí)候。
other = Person.new
other.age = 17 # 預(yù)期結(jié)果:拋出異常
other.sex = 'woman' # 預(yù)期結(jié)果:拋出異常
1.1 getter 和 setter 方法
讓我們從定義一個(gè)標(biāo)準(zhǔn)的age
的getter
和setter
方法開始。
實(shí)例:
class Person
def age= age
@age = age
end
def age
@age
end
end
me = Person.new
me.age = 18
puts me.age
# ---- 輸出結(jié)果 ----
18
1.2 使用eval
來運(yùn)行多行代碼字符串
讓我們把定義class Person
這部分當(dāng)做多行字符串,使用eval
來執(zhí)行。
eval %Q{
class Person
def age= age
@age = age
end
def age
@age
end
end
}
me = Person.new
me.age = 18
puts me.age
# ---- 輸出結(jié)果 ----
18
1.3 動態(tài)賦予類屬性
在類的補(bǔ)丁章節(jié)我們知道了重復(fù)定義類并不會創(chuàng)建同名的類,只會在其基礎(chǔ)上增加實(shí)例方法或類方法。所以我將剛剛定義類的字符串封裝成方法來為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
# ---- 輸出結(jié)果 ----
18
man
1.4 去掉eval
,重構(gòu)方法
使用eval
有時(shí)候并不是一個(gè)好辦法,會影響整體代碼的可讀性和維護(hù)性,因此我們使用class_eval
以及實(shí)例變量set
和get
方法來實(shí)現(xiàn)這個(gè)方法。
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
# ---- 輸出結(jié)果 ----
18
man
注意事項(xiàng):這時(shí)因?yàn)槲覀儸F(xiàn)在不定義Person
類,所以需要在最前面先定義一個(gè)Person
類,否則Ruby會因?yàn)闊o法找到Person
類而報(bào)錯(cuò)。
1.5 增加校驗(yàn)屬性的Block
讓方法對傳入的Block
值進(jìn)行校驗(yàn)
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
# ---- 輸出結(jié)果 ----
18
man
當(dāng)我們賦予屬性的值不滿足條件的時(shí)候會拋出異常。
me = Person.new
me.sex = 'woman'
# ---- 輸出結(jié)果 ----
Invalid attribute! (RuntimeError)
1.6 最后將方法定義到模塊,完成類宏
我們在引入類宏的模塊的時(shí)候使用的是include
,所以我們使用included
鉤子方法,在鉤子方法對引用的類進(jìn)行extend
(因?yàn)?code>extend模塊添加類方法),替代之前的class_eval
,將之前定義屬性的方法定義到被extend
的模塊中,從而使定義的方法可以被類調(diào)用(類方法)。
# 定義模塊部分
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
# ---- 輸出結(jié)果 ----
18
man
當(dāng)我們賦予屬性的值不滿足條件的時(shí)候同樣會拋出異常。
me = Person.new
me.age = 10
# ---- 輸出結(jié)果 ----
Invalid attribute! (RuntimeError)
2. 小結(jié)
在本章節(jié)中,我們一步一步創(chuàng)建一了個(gè)類宏。宏在今后的開發(fā)中會為您省去大量的時(shí)間,大量降低維護(hù)成本和溝通成本。