DEV Community

Cover image for My Ruby Journey: Hooking Things Up
Edison Yap
Edison Yap

Posted on • Originally published at Medium

My Ruby Journey: Hooking Things Up

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

The reason for this is because Ruby's include does not include class methods.

There's 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 wont 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 a included, extended and prepended hooks available for us to (you guessed it) hook into.

source: https://ruby-doc.org/core-2.2.0/Module.html

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 on 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
Enter fullscreen mode Exit fullscreen mode

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 on how 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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

I also posted my article on Medium! Check it out here: https://medium.com/@edisonywh/my-ruby-journey-hooking-things-up-91d757e1c59c

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!

Top comments (8)

Collapse
 
tomk32 profile image
Thomas R. Koll

Nitpicker here, in the ruby world we indent with only two spaces.

As you're learning Rails, have a look at ActiveSupport::Concern, it's used quite a lot in more recent Rails apps.

Keep learning, I'm sure you'll enjoy programming Ruby :)

Collapse
 
edisonywh profile image
Edison Yap

Oh yes definitely! I was copying it from multiple notes app and got messed up haha, thanks for pointing out!

What's your opinion on Concerns? I've heard a lot of people saying concerns are bad, but I've also seen DHH being a big advocate for it.

Collapse
 
tomk32 profile image
Thomas R. Koll

I like them (in Rails) even though they add such little functionality. It's more about the concept of moving code from your models and controllers into separate files that contain a functionality in its entirety. Make those models small again :)

Thread Thread
 
edisonywh profile image
Edison Yap

I see! Definitely agree on slimming down. My company's go-to for slimming down model is always Service Objects, so that's what I've been used to.

I think one of these days I should definitely try out concerns! Thanks for sharing :)

Thread Thread
 
tomk32 profile image
Thomas R. Koll

Serivce Objects and PORO is a must when you write a rake tasks. Processing the input and passing it to the service is all my rake tasks do. Much easier to test.

Collapse
 
tomk32 profile image
Thomas R. Koll

Especially when you're in Japan.

Collapse
 
annarankin profile image
Anna Rankin

Nice work! It's so much fun to dive into the "why" behind these things - and you did quite a nice job of explaining your journey. Thanks!

Collapse
 
edisonywh profile image
Edison Yap

Thanks for the kind words Anna! Glad you liked it :)