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


 

ruby 状态转移_ruby