Guru on Rails

if you don’t sacrifice for your dream then your dream becomes your sacrifice.
Sydney, Sat 01 Jun 2019
Ruby hook methods - meta-programming
Thu 06 Sep 2018

Hook methods is actually the convenient way to help us extend the behaviors of existing class at runtime. In rails there are many popular hook methods we often use such as "included, extended, inherited". We build many modules (mixins in vue.js for example) and allow to include them to classes. 

Hook methods implemented by meta-programming

As we might know about meta-programming in ruby. We usually use class_eval or instance_eval to extend behaviors for classes. But we don't want to use them directly. We develop many methods to do that instead. We make many rooms for developer to put code later. They are the specific method names that we call hook methods. Because it's like a hook, we just need to put code in there without using class_eval or instance_eval.

But don't get stuck in those methods. We can also create our own hook methods. This is an example code of cancancan gem and lazy load hooks in rails (ActiveSupport). on_load is a hook method. We use authorize! methods of cancancan every time need to verify the permissions in rails controller. Have you ever wondered why rails controllers have authorize! method which belongs to cancancan?

# cancancan gem: controller_additions.rb
ActiveSupport.on_load(:action_controller) do
  include CanCan::ControllerAdditions
end

The gem use the on_load hook method to include CanCan::ControllerAdditions at runtime. The miracle happens! We extended the behaviors of rails controllers.

Below is the implementation of on_load hook method.

# ruby ActiveSupport: lazy_load_hooks.rb
def on_load(name, options = {}, &block)
  @loaded[name].each do |base|
     execute_hook(name, base, options, block)
  end

  @load_hooks[name] << [block, options]
end

def execute_hook(name, base, options, block)
  with_execution_control(name, block, options[:run_once]) do
     if options[:yield]
        block.call(base)
     else
        base.instance_eval(&block)
     end
  end
end

Above code we can see that there are two options:

if options[:yield]
   block.call(base)
else
   base.instance_eval(&block)
end

That is about switching the position of the context. 

  • option yield: we put the base object to block to use.
  • otherwise, we use meta-programming, put the block to the context of base instance.

Hook methods implemented by override

There is another way to implement hook methods. That is override. We can see that through template method pattern.

class Engine
	# This is template method
	def prepare
		supply_fuel
		start
		control_speed
	end 
	
	private
	
    # hook methods, this is optional implementation, not like abstract concept.
	def supply_fuel; end
	def start; end
	def control_speed; end
end

We have prepare method is the template method and supply_fuel is Non-abstract method which can be re-implemented in concrete classes or not. We call it hook method. Anyway, hook method is like a hook which allow us to inject new code to existing method. We have two ways to use it:

  • Using override: loading code based on the required order.
  • Using meta-programming: loading code at runtime, in order of processes.

Now we know more one way to apply Open/Closed principle as well. However, there are a bit different with Inheritance. With inheritance we expand the behaviors of superclass by using subclass. But with hook methods, we target using superclass.