Guru on Rails

if you don’t sacrifice for your dream then your dream becomes your sacrifice.
Will Nguyen
Open/Closed principle with hook methods
Thu 13 Sep 2018

I actually intended to add this post to old article about the same title. However, I don't want to make an article too long. Regarding to this principle, there are so many things to learn. Because software design is almost about managing changes. You will see this one as the test at many interviews. The interviewers will give you a small challenge. Yes! pretty easy to solve. Because they just want to see how good you apply this principle. The first requirement might be simple. But be careful. Any time you write code, you need to think that it will never change. We don't write code to change but we write code to extend later. We make a room for changes. 

I won't tell you anythings new. Because the principle is so clear: Closing the change and Open for extending. However, in order to take advantage of it we need samples and practices. Let me show you how rails, especially ActiveJob implemented this one. 

ActiveJob 

At first glance, when I look at "perform" method which we need to implement every time we create a new job class. I wondered why we implement instance method but we involve it like class method? Simply like this.

module ImagesJob
  class DeleteImage < ApplicationJob
    queue_as :default

    def perform(local_images)
      local_images.each do |local_image|
        File.delete(local_image) if File.exists?(local_image)
      end
    end
  end
end

Obviously, perform method here is instance method. But how do we involve?

DeleteImage.perform_later(images)

Where does perform_later method come from? This is how it works in rails. Job class which includes Enqueuing module will involve "new" method to create an instance inside class method "perform_later". We still involve "new" for creating instance but this instance method is hidden in class method. So that we have two options. We can put its instance as a parameter or we delegate job_or_instantiate method to instantiate.

module Enqueuing
    extend ActiveSupport::Concern

    # Includes the +perform_later+ method for job initialization.
    module ClassMethods
      # Push a job onto the queue. The arguments must be legal JSON types
      # (string, int, float, nil, true, false, hash or array) or
      # GlobalID::Identification instances. Arbitrary Ruby objects
      # are not supported.
      #
      # Returns an instance of the job class queued with arguments available in
      # Job#arguments.
      def perform_later(*args)
        job_or_instantiate(*args).enqueue
      end

      private
        def job_or_instantiate(*args) # :doc:
          args.first.is_a?(self) ? args.first : new(*args)
        end
    end
end

This module is included in Base class. 

class Base
    include Enqueuing
end

This is how we learn from above code. We don't use its instance as argument option. We put `self.new` directly in perform_later method.

class ActiveJob
	module ClassMethods
		def perform_later(args = {})
			self.new.perform(args)
		end
	end
	def perform(*)
		raise NotImplementedError
	end
	extend ClassMethos
end

class MyJob < ActiveJob
	def perform(name)
		puts name
	end
end

MyJob.perform_later("Will")
# output: Will

ActionMailer

Using method_missing hook method to involve instance method of subclass. We have to use this hook. Because inherited hook can't recognise the subclass's public methods. It's because the order of involving. Below is my code to simulate how ActionMailer works.

class Mailer
   def self.inherited(subclass)
      subclass.class_eval do 
         def self.method_missing(name, *args)
	        methods = self.public_instance_methods - Object.methods
	        if methods.include?(name)
	           self.new.send(name)
	        else
	           raise "Not found #{name} with #{args}"
	        end
         end
      end
   end
end

class MyMail < Mailer
   def extra_method____________________________________________
      puts "Im new dude"
   end
end

MyMail.extra_method____________________________________________
# output: Im new dude

In fact, ruby on rails uses a lot of hooks methods. Understanding hook methods will help us gain more insights of rails.