Whoa. So Meta. (a little taste of MetaProgramming in Ruby)
First off, what is metaprogramming?
TL;DR…. so,
Metaprogramming is code that writes code.
Neato, now that we’ve got that out of the way, let’s dive into a real use case!
A while back, one of the teams in our company decided to use Pipeline Deals as a sales-CRM-of-sorts to keep track of a few things: the status of a ‘deal’, or what may become a bona-fide Order (or ‘sale’), who the correspondence is with (the client), or User, and what the correspondence has been (the back and forth, emails etc), or, Message.
See where I’m going with this? Pipeline needs some info from our application, and, vice-versa. So, we have three classes (for now, there could be more in the future, right?) that we are working with that are requirements for how we need to connect our app to Pipeline. Right now, those classes are Order, User, and Message.
Since we are (as of now) working with three models, we have created a module to handle the Pipeline-related logic. Modules can be included in models (and included in as many as you want). This gives the model access to all the logic contained in the module. You may see
class Order
include Pipeline
.....
…now any code in the included Pipeline module is now available to any instance of Order.
Here’s our module. For the sake of brevity and not overcomplicating the point I’m trying to get across in this article, I’ve omitted a fair bit of the module’s code. We’ve included this module in the models Order, User and Message:
Okay, yeah. “What’s happening in this module?”, you may ask. Glad you asked!
All these methods basically help us build a request to send off to connect with the Pipeline API. That’s the whole purpose of this Pipeline Module, to send and receive data from our app to Pipeline.
So where’s the “little taste of MetaProgramming in Ruby” you spoke of in the click-baity title? It’s not a lot of code, but it packs a punch. It’s in this method called request.
Okay, so how is this “code that writes code”, you may ask?
Really, there’s two things to notice in this compact method: the interpolated bit (self.class.name), and the constantize method (note, this is a Rails method, not Ruby). We’ll talk a little more about this method below, but first…
What is ‘self’ in this context?
The ‘self’ we see in this ‘request’ method could be one of three things…. got it? Eh?
Right. ‘Self’ is either an instance of Order, User, or Message, the three models we currently have included our module into.
A fairly conventional way to handle this may be something like
if self.class == User
# do x
elsif self.class == Order
# do y
elsif self.class == Message
# and so on
What’s wrong with the above? Not a lot. It’s really just a design choice to not keep building on conditionals (too many conditionals is a code smell), and to do a little metaprogramming instead.
Looking at the interpolated string in the request method, if ‘self’ was coming from the User model, request would return this: ‘Pipeline::UserRequest’
Cool. That’s just a string though. We call constantize on this string which then turns it into a class! We now have a class we can call new on. SWEET. Note, you can’t just make any string name into a class like this. The class has to to exist first. This specific class is already declared in the Pipeline module (see line 24 in the full code snippet above). So, we are good to go.
So now the request method instantiates a class, dynamically, based on whatever self is.
Following along?
the request method now returns an instance of this UserRequest class we’ve defined
Pipeline::UserRequest.new(user)
because self was an instance of User in this case.
So now we could call request.query to pull the needed user’s email from our database, or, request.payload to give us the JSON payload we need to send to Pipeline, and so on. The methods below in UserRequest, again, just help us build a request to the Pipeline API.
You’ll see these methods being used in the create_in_pipeline method. This method makes a POST request to Pipeline, and we are making use of all the methods in the UserRequest class to build a proper request..
Maybe it’d help to see this ‘filled in’. Since our current example is that ‘self’ is an instance of User, we would have this:
So when is a good time to make use of MetaProgramming?
It’s sometimes tempting, when you discover a new trick, to use it all over the place in your code. To start, I think a good place to look for an opportunity to use a little metaprogramming would be in a module, just like we did in this article. Why? Because it can make your code much more extensible, and like mentioned earlier, ‘self’ could be an instance any Model you include your module into, and it can keep you from having to add another conditional every time you include your module into another model.
Alright, we took a quick look at how you could use some metaprogramming to not need to write out conditionals, but return some needed values from a few classes we defined instead. We made some “code that writes code” by taking ‘self’, which could be any object we’ve included our Pipeline module into, and using it to create an instance of some custom classes we’ve defined that hold our information that Pipeline needs.
I hope that peaked your interest a bit into the many ways we could use some metaprogramming in our Ruby/Rails applications. This is just the teeny tip of the iceberg.
To explore some more metaprogramming concepts in Ruby, perhaps start here: https://rubymonk.com/learning/books/2-metaprogramming-ruby/
Hack on, obi-wan.