My Ruby Journey: Hooking Things Up

I’ve only recently just begun my Ruby/Rails journey (worked for little less than a year), and one day I came across something like this.

class Product
acts_as_paranoid
acts_as_taggable
has_paper_trail
monetize :price_cents
end

I knew they came from gems, but I thought it looked super cool and was really curious to how they work, so I took a step back, broke down the problem and tackle it one by one.

Stop 1: Initial Findings

Couple of things I figured out here:

  • They are just normal class methods
  • When you put a method name inside the class, the method gets invoked every time you make a new instance of it.

e.g:

class Example
puts “This gets called whenever you initiate an instance of #{self}”
end
Example.new #=> “This gets called whenever you initiate an instance of Example

So, now that I know that they’re just class methods, I decided that my next step should be to write something similar; Reusable class methods! (I didn’t want to get into how Rails autoload it just yet)

And thus I began my journey, and then voilà, next obstacle arose!

Stop 2: I can’t share class methods with Modules?

(Spoiler: Yes you can, I just didn’t know at first! 😛)

At first, I thought it was going to be a simple `include Module` because that’s how I was told reusable methods work, so here I am, naively trying to define a class method on module then include-ing in the class — but it doesn’t work!

e.g:

module Reusable
def self.awesome?
puts “awesome!”
end
end
class AwesomeObject
include Reusable
end
AwesomeObject.awesome? #=> undefined method `awesome?’ for AwesomeObject:Class (NoMethodError)

After some digging, I figured that the reason for this is because Ruby’s include does not include class methods.

There’s however, a solution through extend, include’s lesser-known sibling (the other one being prepend).

The full explanation is a little bit out of scope for this post, so I won’t dive into it here, but if you want to learn more, it’s due to something called `Singleton Class` in Ruby. This will also be the topic for my next blog post, so stay tuned!

Stop 3: But I want everyone to feel included and hooked!

Since include is a much more popular sibling to extend and prepend, I was quite determined to make sure that the end users only have to remember to include a module, and not having to think —

“Do I extend this, include this or prepend this?”

Also hoping to avoid situation where you include a module, then spending an entire day to figure out why class method doesn’t work.

Actually, in hindsight telling developers to just blindly extend a method is not such a bad idea. But then again, if I gave up then there’d be no blog post now 😉

Stop 4: Wish granted!

After much research, I figured out that Ruby has an included, extended and prepended hooks available for us to (you guessed it) hook into.

These hooks allow us to execute code when a Module is being included, extended or prepended, and guess what’re we going to do?

We’re going to extend a module when it’s being included (this sounded funny to me at first)

Let’s take a look at a small example of how hooks work:

e.g:

module HookedModule
def self.included(base)
puts "#{self} is being included in #{base}"
end
end
class BaseObject
include HookedModule
end
BaseObject.new #=> HookedModule is being included in BaseObject

Couple things to note here:

  • included needs to be a class method because it’s a method on Module class. If you don’t declare it a class method, the hook doesn’t work.
  • The base argument being passed into included is the class that you’re including the module from, which is BaseObject in our case.

Final Stop: Extend? Include? Why not both?

Now that I have a better understanding of how Rubys’ hooks work, let’s read back what my goal was:

  • Extend a module when it’s being included

Awesome! Let’s try to extend class methods there! (Don’t you just love it when the requirement tells you exactly what you need to do?)

module HookedModule
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def awesome?
puts "Yes, #{self} is awesome, stop asking."
end
end
end
class BaseObject
include HookedModule
end
BaseObject.awesome? #=> "Yes, BaseObject is awesome, stop asking.

The reason this work is because when we first hook into included, we gained access to our BaseObject through base. We can then call extend (a class method) on our BaseObject class, which works exactly the same as if we had just done it like so:

class BaseObject
extend HookedModule
end

This seems like a lot of work at first, but if you want to reuse instance methods and also class methods, it makes no sense to have to both include and extend YourModule.

Recap

So, I started off the article trying to figure out how these gems provide magic methods — I still do not fully understand how the methods are being loaded without explicitly including or excluding a module (perhaps a topic for another blog post!), but I found out that they are all just normal class methods, and I could use this along with Module to make my class methods reusable.

My Learnings:

  • You can run methods on initialization of classes.
  • including a module does not include class methods.
  • Module has hooks which we can hook into to simplify our API.

Author’s Note

This is the first blog post I’ve ever written! Might not have been exactly the most advanced nor the most well-written post, but I’m learning still, so please do drop a comment if you have any feedback, I’d really appreciate them!

So that’s it! My first Ruby Journey coming to a close. I love exploring through Ruby’s little magic here and there, so I’ll hopefully be writing a few more blog posts down the line. Until then, have a safe journey through your own Ruby land!