ruby 狀態轉移

0. 引言

      昨天遇到一個問題,就是關於對象狀態轉移的問題,我姑且這樣命名吧。簡要描述一下就是:對於一個人,他有進食,幫助他人,戀愛等功能,但是這些功能是有先後順序的,對於剛出生的人,他要先學會進食,然後隨着他的成長,他逐漸學會幫助他人,在這個過程中他學會了愛與被愛,當他遇到一個合適的女孩,他就墜入了愛河。整個過程反映到程序上就是,必須按照下面的順序調用方法:
man=Human.new
man.feed
man.fall_in_love   # Error
man.help_people
man.fall_in_love

     如果你調用某個功能時沒有完成前面的事情,就像上面的例子這樣,一個人尚未學會幫助他人的人,我們是不希望他去戀愛的,這樣一個不懂得互助互愛的人怎麼可能珍惜自己的愛人呢?
     所以,對象狀態轉移就是:某個對象隨着狀態轉移獲得調用新方法的能力或權限,未達到某個狀態前無法調用該狀態下的方法。

1. 目標

      仔細想想,其實這類問題出現還是比較普遍的,比如一個瀏覽器處理類,它必須要在登陸操作後才允許執行修改個人信息。所以,有必要爲了這類問題思考一個解決方法。那麼,首先要明確的是,我想要怎樣實現這樣一個功能。爲每個類去實現一個這樣的狀態轉移顯然不是ruby way。所以,我覺得對於我自己,我希望這樣處理一下我的Human類之後,我就能像引言中那樣直接使用狀態轉移提供的功能:
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中,我希望可以包含列表,列表中的方法需要至少被調用一種才能執行方法鏈的後續調用。
      好吧,這樣看起來,似乎是像模像樣的ruby解決方法了,那麼,下面就看看如何來實現這個State模塊。

2. 環繞別名

     首先,我們肯定需要在define_chain方法上做文章。該方法實際完成狀態轉移方法鏈的定義,那麼問題的關鍵是:我知道了這一串方法,怎麼樣保證在調用下一個方法前,明確上一個方法是否被調用了呢?很顯然,我們需要一個變量來保存狀態,在每次調用方法前檢查是否能夠調用當前方法,如果能夠,則在調用完成之後更新狀態。那麼怎麼做呢?總不能要求編寫Human類的程序員在每個方法調用前先檢查一下狀態,在調用完成後再更新狀態吧,那顯然是會被鄙視的。實際上,作爲一個ruby程序員,每個人都需要會一點點魔法,這次的魔法就是環繞別名。
    假如,對於某個方法名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
	end
alias_method "#{method}_out_chain",method
alias_method method,"#{method}_in_chain"
     這部分代碼就是define_chain方法的主體,這樣,在定義了狀態轉移方法鏈之後,直接調用在方法鏈中的方法,就會自動使用validate_state_for方法檢查方法是否可以被調用,在完成調用後使用update_state_for方法更新狀態。
   然後我們去實現validate_state_for和update_state_for方法,這兩個方法實現很簡單,後面再說,我們的State模塊看起來基本是這樣的:
module State
	def define_chain(*args)
	end

	def validate_state_for(method)
	end
	
	def update_state_for(method)
	end
end


    好吧,問題的最關鍵部分解決了,但還是有一些細節,不要小看細節,它決定成敗。

3. 類擴展混入

   顯然,我們的define_chain方法必須作爲類方法存在,這很簡單,可以使用擴展混入。即
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. 最後一步,實現

     我們的State狀態轉移模塊的結構就是這樣了,那麼下面就需要具體實現了。
    狀態判斷邏輯非常簡單:按照狀態方法鏈的定義,從左到右從0開始編號,而對象狀態也從0開始,僅到當前狀態大於等於方法編號時,才允許調用該方法。
     狀態更新邏輯:僅當狀態方法編號等於當前對象狀態時,才更新狀態,即將狀態值加1。
     這就是State模塊的實例方法實現:
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具體結構馬上就會看到。
    下面就是State::StateMaker的的define_chain方法的實現:
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. 結尾

       現在,整個狀態轉移方法調用就完成了,可以像引言中那樣去使用了。不過,這僅僅是個開始,ruby的原則就是DRY,還有細節的地方需要完善修改,比如用ruby2.0就可以更漂亮地完成環繞別名等等。

6. 附錄

     下面是State模塊完整代碼,供參考。
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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章