Appreciating Rails - Part 1.2 : Model Callbacks

The part where we complete the other model callbacks using meta programming to reduce the amount of similar looking code.

Ruby has a very nice define_method method, which allows us to dynamically define methods.

Using that, we can rewrite :

    def before_save_callbacks
      @_before_saves
    end

    def after_save_callbacks
      @_after_saves
    end

    def around_save_callbacks
      @_around_saves
    end

    def before_save(*args)
      add_before_save_callback(*args)
    end

    def after_save(*args)
      add_after_save_callback(*args)
    end

    def around_save(*args)
      add_around_save_callback(*args)
    end

    private

    def add_before_save_callback(*args)
      (@_before_saves ||= []) << args
    end

    def add_after_save_callback(*args)
      (@_after_saves ||= []) << args
    end

    def add_around_save_callback(*args)
      (@_around_saves ||= []) << args
    end

such that our final module looks like this :

    module ClassMethods

    [:before, :after, :around].each do |cb_when|
      define_method("#{cb_when}_save_callbacks".to_sym) do
        self.instance_variable_get("@_#{cb_when}_saves")
      end
      define_method("#{cb_when}_save".to_sym) do |*args|
        send("add_#{cb_when}_save_callback".to_sym, *args)
      end
      define_method("add_#{cb_when}_save_callback".to_sym) do |*args|
        if prev = self.instance_variable_get("@_#{cb_when}_saves")
          self.instance_variable_set("@_#{cb_when}_saves", (prev << args))
        else
          self.instance_variable_set("@_#{cb_when}_saves", [args])
        end
      end
      private "add_#{cb_when}_save_callback".to_sym
    end
  end

Now similarly adding the update, create, destroy and commit callbacks. Making the final module look like :

module ClassMethods  
    [:save, :update, :create, :commit, :destroy].each do |cb|
      [:before, :after, :around].each do |cb_when|
        define_method("#{cb_when}_#{cb}_callbacks".to_sym) do
          self.instance_variable_get("@_#{cb_when}_#{cb}s")
        end
        define_method("#{cb_when}_#{cb}".to_sym) do |*args|
          send("add_#{cb_when}_#{cb}_callback".to_sym, *args)
        end
        define_method("add_#{cb_when}_#{cb}_callback".to_sym) do |*args|
          if prev = self.instance_variable_get("@_#{cb_when}_#{cb}s")
            self.instance_variable_set("@_#{cb_when}_#{cb}s", (prev << args))
          else
            self.instance_variable_set("@_#{cb_when}_#{cb}s", [args])
          end
        end
        private "add_#{cb_when}_#{cb}_callback".to_sym
      end
    end
  end

Note the only difference is the addition of

[:save, :update, :create, :commit, :destroy].each do |cb| ... end block

Some questions to ask :

Where the hell does define_method come from ?
On which object does it define the methods ?
What exactly does define_method do ?
What's the scope of these 'defined' methods ?

Where does it come from :

Since we are able to call define_method within our class, it means its available as a class method.
Our class ClassMethods (or any class for that matter) is an instance of the class Class ( yes, I know it's confusing. sadly, it doesn't get better ).

If you check Class.superclass , you'll find that Class inherits from Module. And if you check Module.private_methods , you would find define_method sitting right there.
On which object does it define the methods ?

Since we found that define_method is a private instance method of Module. And Class < Module. Therefore only instances of Class can call define_method. (That too without an explicit receiver, which means you can not do self.define_method inside the class, but just define_method ... )

And any class defined as class Abc; end is an instance of Class. Therefore you can call define_method on class scope for Abc.
And that's why we are able to call define_method inside the class body, as inside the class body, the scope is of the class.

What exactly does define_method do ?

For our use case, let's just say it defines a method with the name provided, whose body is instance_eval of the block that we passed to define_method.

What's the scope of these methods ?

Since these methods are defined by instance_eval on the class object, in our case ClassMethods, hence these become instance methods of ClassMethods class.

The code is on Github.

If you'd like to discuss this more, or If I made a boo boo, please let me know. I'm @murtazz on Twitter.

Murtuza Kutub

Read more posts by this author.

Chennai, India

Subscribe to Murtuza's Blog

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!