In a previous article, I shared how I automatically initialize variables for my Rails console sessions. In step 3 of that post, “Create your variables!”, we declared several constants in an initialization file that we could then access in the Rails console.
After reviewing this code with my friend Peter Kajpust, we discovered yet another way to set up those same variables, this time using a class-based approach. This strategy is much more robust and allows for even more expressive data. Let’s take a look!
The same set up instructions apply from the previous post, “How to Automatically Initialize Variables for Every Rails Console Session”. You’ll specifically need to create a pryrc file (or irbrc, depending on your Rails setup) and some global Git ignore configurations, all of which are covered in detail in that article. So, head over there and come back here when you are ready to initialize your variables in your init file 🤙🏽.
In our init file, let’s start by building a class to access our Rails application and declare some getter methods. We can then declare a constant assigned to an instance of our class that has some test data. We will give our instance real data in a moment, so hold yer horses!
```ruby
class CarScope
attr_accessor :user, :car, :type
def initialize(user:, car:, type:)
@user = user
@car = car
@type = type
end
end
C = CarScope.new(
user: 'Sunjay',
car: 'Tesla',
type: 'Electric',
)
```
- `attr_accessor :user, :car, :type` is a nifty shortcut to create simple getters in a Ruby class. Thus, `C` has access to `user`, `car`, and `type`. For example, `C.user` will return `Sunjay`.
- The initialize method takes in three keyword arguments (also called “named” arguments): `user`, `car`, and `type`. Our instance variables (for example `@user`) are then set to the arguments passed in during instantiation. Our getters, in this case, return the value from their corresponding instance variable.
Sweet, let’s replace the test data with real data from our application:
```ruby
class CarScope
attr_accessor :user, :car, :type
def initialize(user:, car:, type:)
@user = user
@car = car
@type = type
end
end
user = User.find_by(role: 'admin')
car = user.car
type = car.type
C = CarScope.new(user: user, car: car, type: type)
```
Now if we fire up our Rails console, we can access our variables using the getter methods on our class.
```ruby
$ bin/rails c
Loading development environment (Rails 6.1.7.3)
[1] pry(main)> C.user.email # Returns "sunjaymunjay@dev.int"
```
We could stop here, but what if we used our class to do even more than return instantiated data?
Let’s say I’m working on a feature that digs extremely deep into my model relationships. I could store that as a variable in my Rails console session:
```ruby
$ bin/rails c
Loading development environment (Rails 6.1.7.3)
[1] pry(main)> manufacturer_address = C.car.part.dealer.manufacturer.plant.address
```
Declaring console variables like this is not very expressive, though. Thankfully, Ruby’s `BasicObject` (the great ancestor of our `CarScope` class*) calls the `method_missing` method any time an object is “sent a message it cannot handle”. We can override that method to store variables in the instantiated class on the fly.
`method_missing` can take in a number of arguments, but the ones most useful to us here are the method name invoked and any additional arguments passed in. Let’s start with a simple `puts` of the method name and arguments:
```ruby
class CarScope
attr_accessor :user, :car, :type
def initialize(user:, car:, type:)
@user = user
@car = car
@type = type
end
def method_missing(method_name, *args)
puts method_name
puts args
end
end
user = User.find_by(role: 'admin')
car = user.car
type = car.type
C = CarScope.new(user: user, car: car, type: type)
C.karate_kick = 'Hyah!' # Prints 'karate_kick' and 'Hyah'
```
When `C.karate_kick` is called, Ruby fires off `method_missing` since the `karate_kick` method doesn’t exist on our `CarScope` class or its ancestors. Our `CarScope` class overrides the `method_missing` method from its parent by printing out the method name. Ruby is then smart enough to know that `Hyah!` is among the `*args`.
From here, we need a way to create a getter and setter whenever `method_missing` is called on `CarScope`. Doing so will allow us to store methods and variables on the class during our console session. We are essentially creating a public interface for our class on the fly.
So, how exactly does Ruby typically create getters and setters? Let’s take a look at the `attr_accessor` method. In the documentation, we see that `attr_accessor` creates two variables for each symbol it is passed. For instance, in our `CarScope` class we declare `attr_accessor :user`. Ruby will create `user` to allow us to read the data stored in that variable. Ruby will also create `user=` to allow us to set data in that variable.
When accessing variables from outside the class, reading the data is as simple as `C.user`, while setting the data typically looks like `C.user = "Your Username"`. But do you see the equal sign? That hints that `C.user=("Your Username")` also does the same thing.
Let’s not depart too far from this setting paradigm and check for assignment operations on our `CarScope` class.
```ruby
class CarScope
# Code omitted for brevity
def method_missing(method_name, *args)
match = method_name.match(/(.+)=/)
end
end
```
Here we are passing `match` a Regular Expression.
<ul>
<li><code>/</code> is the start of the expression.</li>
<li>The parenthesis<code>(</code> and <code>)</code>form a capture group for better referencing.</li>
<li><code>.+</code> inside the parenthesis essentially matches one or more of any character.</li>
<li><code>=</code> is the equal sign we are looking for!</li>
<li>The final <code>/</code> ends the expression.</li>
<li>Altogether, our RegEx is looking for anything before an equal sign. So, anytime we call an unknown method with an equal sign, our class understands that we want to assign it some data. Neato!</li>
<li style="font-style: italic;">By the way, I like to use <a href="https://regexr.com/">RegExr</a> to understand complex expressions. It’s a wonderful (and free!) tool.</li>
</ul>
Alright, now we have a match (pun intended). `match` is an array with two indicies, the final one being the name we wish to call as a method on our class in the future. Ruby provides us with `instance_variable_set` and `instance_variable_get` to dynamically create getters and setters. Both methods take in the name of the getter or setter, and the data to set or get when called.
Let’s give it a try:
```ruby
class CarScope
# Code omitted for brevity
def method_missing(method_name, *args)
match = method_name.match(/(.+)=/)
if match.present?
instance_variable_set("@#{match[1]}", args[0])
else
instance_variable_get("@#{method_name}")
end
end
end
```
Basically, if we determine that an assignment operation is happening (that is, if `match.present?`), then we set an instance variable with the matched name and its arguments. If not, we use a getter to grab the data from that same method name.
In the case of our getter, its important to note that `match` will return an empty array. That’s because we call getters without an equal sign (e.g. `C.karate_kick`) and is why we specifically use the `method_name` (not `match[1]`) to request data from our class.
I generally don’t recommend overriding the `method_missing` method on objects within production code. Doing so could lead to challenging debugging and unforeseen side effects. In our case, we are overriding `method_missing` on a class in an initialization script that is not used in production, so we should be okay. Just be cautious when overriding methods like this, folks!
Using classes, you can save yourself a lot of time by automatically initializing constants, utilizing metaprogramming techniques, and even creating reusable methods for your Rails console sessions. So identify those repetitive code chores and outsource them to Ruby objects! I hope you have fun optimizing your workflows 👋🏽
<hr style="width: 100%;" />
<h3 id="footnote">Footnotes</h3>
* Inheritance is a core part of object oriented programming. I highly recommend Chapter 6: “Acquiring Behavior through Inheritance” of Practical Object-Oriented Design: An Agile Primer Using Ruby by Sandi Metz for more on this subject. You can also perform fun code experiments with the ancestors method available on all Ruby objects.
Photo by Daniel K Cheung on Unsplash
Are you ready to build something brilliant? We're ready to help.