0. introduction I encountered a problem yesterday about object state transfer. Let me name it like this. A brief description is: for a person, he has functions such as eating, helping others, and falling in love, but these functions are sequential. For a person born, he must learn to eat first, then as he grew up, he gradually learned to help others. In this process, he learned to love and be loved. When he met a suitable girl, he fell in love. The whole process is reflected in the program and the method must be called in the following order:
man=Human.newman.feedman.fall_in_love # Errorman.help_peopleman.fall_in_love
If you do not complete the previous tasks when calling a function, as in the above example, if a person has not yet learned to help others, we do not want him to fall in love, how can such a person who does not know mutual assistance and mutual love cherish his lover? Therefore, object state transfer means that an object obtains the ability or permission to call a new method as the state changes. A method in this state cannot be called before it reaches a certain state. 1. Think about the goal. In fact, this type of problem is still quite common. For example, for a browser processing class, it must be allowed to modify personal information after login. Therefore, it is necessary to consider a solution to such problems. The first thing to clarify is how I want to implement such a function. Implementing such state transfer for each class is obviously not a Ruby way. Therefore, I think for myself, I hope that after processing my human class like this, I will be able to directly use the functions provided by status transfer as in the introduction:
class Humaninclude Statedef feedputs"feed myself"enddef protect_envputs "protect environment"enddef help_peopleputs "help other people"enddef fall_in_loveputs "love someone"enddefine_chain :feed,[:protect_env,:help_people],:fall_in_loveend
As shown in the Code, if I want to include a State module in the class I use, and then define a method chain using define_chain, then the methods in the method chain, you must call the previous method before it can be called. Otherwise, an exception is thrown. In addition, in define_chain of the method chain, I want to include the list. The method in the list must be called at least one to execute the subsequent call of the method chain.
Well, it seems like a decent Ruby solution, so let's look at how to implement this state module. 2. Surround aliases first, we certainly need to post on the define_chain method. The key to the problem of actually completing the definition of the State transfer method chain in this method is: I know this method string, how to ensure that before calling the next method, are you sure you have called the previous method? Obviously, we need a variable to save the status and check whether the current method can be called before each call. If yes, the status will be updated after the call is complete. How can this problem be solved? There is always no need for human programmers to check the status before calling each method, and then update the status after the call is complete, which is obviously despised. In fact, as a ruby programmer, everyone needs a little magic. This magic is a circle alias. Suppose we can wrap a method named method like this:
define_method "#{method}_in_chain" do |*params,&block|validate_state_for method.to_symself.send "#{method}_out_chain",*params,&blockupdate_state_for method.to_symendalias_method "#{method}_out_chain",methodalias_method method,"#{method}_in_chain"
This part of the code is the subject of the define_chain method. In this way, after defining the state transfer method chain, you can directly call the method in the method chain, the validate_state_for method is automatically used to check whether the method can be called. After the call is completed, the update_state_for method is used to update the status.
Then we will implement the validate_state_for and update_state_for methods. The implementation of these two methods is very simple. Later, our state module looks like this:
module Statedef define_chain(*args)enddef validate_state_for(method)enddef update_state_for(method)endend
Well, the most important part of the problem is solved, but there are still some details. Don't underestimate the details. It determines success or failure. 3. Obviously, our define_chain method must exist as a class method, which is very simple and can be mixed with extension. That is
class Human extend Stateend
But the problem is that I only want define_chain to be mixed in as a class method, while the validate_state_for and update_state_for methods still need to be used as class instance methods. If you mix them directly, you will not be able to use another Ruby magic-class extension to mix some methods into the class method, and some methods into the instance method. This magic uses the extended hook.
module Statedef self.included(base)base.extend StateMakerendmodule StateMakerdef define_chain(*args)endenddef validate_state_for(method)enddef update_state_for(method)endend
Now, you can use the following method to get the desired effect. I can use the class method define_chain to define the state method chain, or instantiate the human object to call its validate_state_for instance method.
class Human include Stateend
4. In the last step, the structure of the state transition module is as follows. The State judgment logic is very simple: according to the definition of the state method chain, numbers are numbered from left to right from 0, and the object state also starts from 0, only when the current state is greater than or equal to the Method number, to call this method. Status update logic: the status is updated only when the status Method number is equal to the current object status. The status value is added to 1. This is the implementation of the instance method of the state module:
module Statedef validate_state_for(method)raise "State is too low to execute #{method}" unless min_state_for(method) <= stateenddef min_state_for(method)self.class.state_chain.find_index{|k,v| v.include? method}enddef update_state_for(method)@_state_from_object_monitor_+=1 if min_state_for(method) == stateenddef reset_state@_state_from_object_monitor_=0enddef state@_state_from_object_monitor_=0 unless @_state_from_object_monitor_@_state_from_object_monitor_endend
This module also provides the reset_state method to reset the status value. In addition, the min_state_for method is used to obtain the lowest state value for calling a method. In this method, Ruby is used for a little magic. The class instance variable state_chain is a class method, it gets a hash table that defines the State transfer method chain. This table is a class instance variable, and the specific hash structure will be seen immediately. The following is the implementation of the define_chain method of statemaker:
module Statemodule StateMakerdef 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_symself.send "#{method}_out_chain",*params,&blockupdate_state_for method.to_symnilendalias_method "#{method}_out_chain",methodalias_method method,"#{method}_in_chain"end@chain_methods=args.each_with_index.inject({}) do |memo,(v,index)|memo[index]=v.class==Symbol ? [v] : vmemoendnilenddef state_chain@chain_methodsendendend
The first half of the define_chain method uses the wrap alias to wrap the specific method. The second half is the hash table of the method chain. The generated hash table is saved in the instance variable @ chain_methods, because define_chain is mixed in as a class method, it naturally becomes a class instance variable mixed into the class. Note that you should try to use more class instance variables instead of class variables. The state_chain method is also incorporated into the class method, which is purely used to obtain the chain_methods variable of the class instance. 1. The structure of the hash table generated by the method chain in the target is:
{0=>[:feed], 1=>[:protect_env, :help_people], 2=>[:fall_in_love]}
5. Now, the call of the entire state transfer method is complete and can be used as in the introduction. However, this is just the beginning. The ruby principle is "dry", and the details need to be improved. For example, ruby2.0 can be used to complete the surrounding alias more beautifully. 6. The complete code of the state module is shown in the appendix for your reference.
module Statedef self.included(base)base.extend StateMakerendmodule StateMakerdef 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_symresult=self.send "#{method}_out_chain",*params,&blockupdate_state_for method.to_symresultendalias_method "#{method}_out_chain",methodalias_method method,"#{method}_in_chain"end@chain_methods=args.each_with_index.inject({}) do |memo,(v,index)|memo[index]=v.class==Symbol ? [v] : vmemoendnilenddef state_chain@chain_methodsendenddef validate_state_for(method)raise "State is too low to execute #{method}" unless min_state_for(method) <= stateenddef min_state_for(method)self.class.state_chain.find_index{|k,v| v.include? method}enddef update_state_for(method)@_state_from_object_monitor_+=1 if min_state_for(method) == stateenddef reset_state@_state_from_object_monitor_=0enddef state@_state_from_object_monitor_=0 unless @_state_from_object_monitor_@_state_from_object_monitor_endend