0. 引言
man=Human.new
man.feed
man.fall_in_love # Error
man.help_people
man.fall_in_love
如果你調用某個功能時沒有完成前面的事情,就像上面的例子這樣,一個人尚未學會幫助他人的人,我們是不希望他去戀愛的,這樣一個不懂得互助互愛的人怎麼可能珍惜自己的愛人呢?
1. 目標
class Human
include State
def feed
puts"feed myself"
end
def protect_env
puts "protect environment"
end
def help_people
puts "help other people"
end
def fall_in_love
puts "love someone"
end
define_chain :feed,[:protect_env,:help_people],:fall_in_love
end
如代碼所示,我希望在我使用的類中包含一個State模塊,然後用define_chain定義一個方法鏈,那麼方法鏈中的方法,必須要在前一個方法調用過之後纔可以被調用,否則就會拋出異常。另外,在定義方法鏈的define_chain中,我希望可以包含列表,列表中的方法需要至少被調用一種才能執行方法鏈的後續調用。2. 環繞別名
define_method "#{method}_in_chain" do |*params,&block|
validate_state_for method.to_sym
self.send "#{method}_out_chain",*params,&block
update_state_for method.to_sym
end
alias_method "#{method}_out_chain",method
alias_method method,"#{method}_in_chain"
這部分代碼就是define_chain方法的主體,這樣,在定義了狀態轉移方法鏈之後,直接調用在方法鏈中的方法,就會自動使用validate_state_for方法檢查方法是否可以被調用,在完成調用後使用update_state_for方法更新狀態。module State
def define_chain(*args)
end
def validate_state_for(method)
end
def update_state_for(method)
end
end
好吧,問題的最關鍵部分解決了,但還是有一些細節,不要小看細節,它決定成敗。
3. 類擴展混入
class Human
extend State
end
但問題來了,我只希望define_chain被作爲類方法混入,而validate_state_for和update_state_for方法仍然需要作爲類實例方法。那麼直接混入肯定就不行了,這時就需要使用ruby另一個魔法了——類擴展混入,將部分方法作爲類方法混入,部分方法作爲實例方法混入。這種魔法使用了included鉤子。
module State
def self.included(base)
base.extend StateMaker
end
module StateMaker
def define_chain(*args)
end
end
def validate_state_for(method)
end
def update_state_for(method)
end
end
現在,在使用下面的方法混入,就獲得了我想要的效果。我能夠用類方法define_chain定義狀態方法鏈,也能夠實例化Human對象調用它的validate_state_for實例方法。
class Human
include State
end
4. 最後一步,實現
module State
def validate_state_for(method)
raise "State is too low to execute #{method}" unless min_state_for(method) <= state
end
def min_state_for(method)
self.class.state_chain.find_index{|k,v| v.include? method}
end
def update_state_for(method)
@_state_from_object_monitor_+=1 if min_state_for(method) == state
end
def reset_state
@_state_from_object_monitor_=0
end
def state
@_state_from_object_monitor_=0 unless @_state_from_object_monitor_
@_state_from_object_monitor_
end
end
該模塊還提供了reset_state方法重置狀態值。另外,min_state_for方法用於獲取調用某個方法的最低狀態值,該方法中實際上也使用了ruby一點點小魔法,類實例變量,state_chain是一個類方法,它獲取了是我們定義的狀態轉移方法鏈的一個hash表,該表是一個類實例變量,這個hash具體結構馬上就會看到。
module State
module StateMaker
def define_chain(*args)
args.map{|x| x}
args.flatten.each do |method|
define_method "#{method}_in_chain" do |*params,&block|
validate_state_for method.to_sym
self.send "#{method}_out_chain",*params,&block
update_state_for method.to_sym
nil
end
alias_method "#{method}_out_chain",method
alias_method method,"#{method}_in_chain"
end
@chain_methods=args.each_with_index.inject({}) do |memo,(v,index)|
memo[index]=v.class==Symbol ? [v] : v
memo
end
nil
end
def state_chain
@chain_methods
end
end
end
define_chain方法的前半部分使用環繞別名來包裹特定方法,後半部分就是生成方法鏈的hash表,生成的hash表被保存在實例變量@chain_methods中,由於define_chain被作爲類方法混入,所以它自然也成爲了混入類的類實例變量,注意,儘量多使用類實例變量而不要使用類變量。而state_chain方法也同時混入成爲類方法,該方法純粹就是用來獲取類實例變量chain_methods的。如1.目標中的方法鏈生成的hash表的結構是:
{0=>[:feed], 1=>[:protect_env, :help_people], 2=>[:fall_in_love]}
5. 結尾
6. 附錄
module State
def self.included(base)
base.extend StateMaker
end
module StateMaker
def define_chain(*args)
args.map{|x| x}
args.flatten.each do |method|
define_method "#{method}_in_chain" do |*params,&block|
validate_state_for method.to_sym
result=self.send "#{method}_out_chain",*params,&block
update_state_for method.to_sym
result
end
alias_method "#{method}_out_chain",method
alias_method method,"#{method}_in_chain"
end
@chain_methods=args.each_with_index.inject({}) do |memo,(v,index)|
memo[index]=v.class==Symbol ? [v] : v
memo
end
nil
end
def state_chain
@chain_methods
end
end
def validate_state_for(method)
raise "State is too low to execute #{method}" unless min_state_for(method) <= state
end
def min_state_for(method)
self.class.state_chain.find_index{|k,v| v.include? method}
end
def update_state_for(method)
@_state_from_object_monitor_+=1 if min_state_for(method) == state
end
def reset_state
@_state_from_object_monitor_=0
end
def state
@_state_from_object_monitor_=0 unless @_state_from_object_monitor_
@_state_from_object_monitor_
end
end