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)
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.
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
Couple things to note here:
-
included
needs to be a class method, because it's a method onModule
class. If you don't declare it a class method, the hook doesn't work. - The
base
argument being passed intoincluded
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 beingincluded
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!
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)
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 :)
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.
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 :)
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 :)
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.
Especially when you're in Japan.
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!
Thanks for the kind words Anna! Glad you liked it :)