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