Object-Oriented Ruby and My First Ruby Project–Quotes App

Luis Martinez
10 min readJan 30, 2021
Two dices made of ruby each with 12 sides; first dice shows a 6; second dice shows a 12.
Two hypothetical ruby objects (two regular dodecahedrons). We could hypothetically say that for each object each face represents an attribute for the object. Image source: https://www.google.com/search?q=dice+roller

I first learned about Object-Oriented Ruby while completing the free online Software Engineer boot-camp prep course from Flatiron. I’m now a student at Flatiron in the self-paced Software Engineer program. As part of the first project (out of 5 projects) in the curriculum, we needed to build a ruby command-line-interface (CLI) that applies the fundamental concepts from Object-Oriented Ruby.

After doing research, I decided to build a CLI that displays quotes from various authors and categories.

I‘ll devote the rest of the time to discuss some of the obstacles I faced and what I learned from the project.

Mass Assignment and the initialize method

Loading Databases and Code Refactoring

Mass Assignment and the Initialize Method

For my project, I knew I wanted to create quote objects with several attributes, and that I wanted to create many quote objects at once. So I went on to apply mass assignment. In mass assignment, we iterate over a hash to set the key-value pair(s) of the hash as the corresponding attribute–attribute return value pair(s) of the instance of a class. This is done via the .send method. The .send method operates on an instance of a class, and takes two arguments: the first argument is the name of a setter method for an object (ex: object.color =), and the second argument is the value for the assigned setter method (ex: object.color = "blue”). Hence, supposing we have an Object class with an attribute accessor of color, calling .send on an instance of Object to set the value of the color property of the instance equal to the string ‘blue’ can be done as follows:

object = Object.newobject.send("color=", "blue")

The last line of the above code is equivalent to object.color = “blue”. For my Quote class in my project, I wanted to set values for each of the attributes of my Quote class’ instances upon initialization of the instances. Hence I used the following code:

class Quote
attr_accessor :body, :author
@@all = [ ] def initialize(quote_hash)
quote_hash.each do |key, value|
self.send(("#{key}="), value)
end
@@all << self
end
# more code...
end

As explained before, the .send method has two arguments. Interpolation is used in the first argument ("#{key}=") in order to set the incoming hash’s key as a setter method for the instance that is being initialized. Also, it is worth noting that the incoming key needs to be included as an attribute accessor in the definition of the object’s class. The argument of the above initialize method is a hash. An example of one such hash (which are the precursors of the instances of my Quote class) would look like this:

{:body=>"I didn't fail the test. I just found 100 ways to do it wrong.",
:author=>"Benjamin Franklin"}

Since I wanted to create many quote objects at once, I needed to have an array of hashes, where the key-value pairs from each hash would be used to instantiate a corresponding new quote object. I got my array of hashes using my Scraper class.

Let’s assume that I got the below array of two hashes from my Scraper class:

quotes_array = [
{:body=>"I didn't fail the test. I just found 100 ways to do it wrong.",
:author=>"Benjamin Franklin"},
{:body=>
"Always bear in mind that your own resolution to success is more important than any other one thing.",
:author=>"Abraham Lincoln"}
]

My goal was (in this case) to create one quote object from each hash, with body and author properties set equal to their corresponding values from the hash upon initialization of the corresponding quote objects. So my first attempt was this:

Quote.new(quotes_array)

and I got the following error:

NoMethodError: undefined method `{:body=>"I didn't fail the test. I just found 100 ways to do it wrong.", :author=>"Benjamin Franklin"}=' for #<Scraper::Quote:0x0000000001f33558> from (pry):89:in `block in initialize'

In the beginning, I thought that, since the quotes_array contained hashes, the .each enumerator within the initialize method in my Quote class would iterate upon the keys and values of each hash in the quotes_array:

quotes_array.each do |key, value| 
self.send(("#{key}="), value)
end

To better understand why I got the previous error, let us rewrite the quotes_array as follows:

quotes_array = [hash_1, hash_2]

where hash_1 is the hash that has Benjamin Franklin as the value of the author key, and hash_2 is the second hash. The first iteration from the .each enumerator would use the element hash_1 as a setter method on the instance of the Quote class being instantiated at the moment (the error message give us that specific Quote class instance,Scraper::Quote:0x0000000001f33558), that is, the block for .each is doing the following:

# hash_1 is passed in the argument of the .each enumerator, encapsulated within the 'key' element:   quotes_array.each do |key, value| 
self.send(("#{key}="), value)
end
# A new instance of the Quote class is initialized (that is, `self` becomes a new object): quotes_array.each do |key, value|
Scraper::Quote:0x0000000001f33558.send(("#{key}="), value)
end
# Because there is not a call for 'key.keys' or 'key.values' within the block for .each, neither the keys nor the values from hash_1 are accessed, and the only element for iteration is hash_1 itself; hence |key, value| becomes |key| = |hash_1| # and so we get quotes_array.each do |key|
Scraper::Quote:0x0000000001f33558.send(("#{key}="), value)
end
# The .send method will set 'key' as a setter method for the newly created Quote object, that is, .send will do the following: Scraper::Quote:0x0000000001f33558.key =# Because key is equal to hash_1, the previous line can be rewritten as Scraper::Quote:0x0000000001f33558.hash_1 = # Since hash_1 is not an attribute in the definition of our Quote class, at this point we get an error saying that the .hash_1= (setter) method is not defined for our instance of the Quote class. Finally, because hash_1 is originally defined as {:body=>"I didn't fail the test. I just found 100 ways to do it
wrong.",
:author=>"Benjamin Franklin"}
we get the error message from terminal, which now clearly outputs that 'hash_1=', namely, `{:body=>"I didn't fail the test. I just found 100 ways to do it
wrong.", :author=>"Benjamin Franklin"}=
'
is an undefined method for the object
#<Scraper::Quote:0x0000000001f33558>

So, if we pass in an array of hashes as an argument in the initialize method, the keys and the values of the hashes will not be accessed, and the block for .each will try to set each iterated hash as the setter method for the initialized instance of the Quote class. Therefore, we will need a second iteration in order to go one level deeper and access the keys and values from each hash. To accomplish this, I used the below code:

class Quote
attr_accessor :body, :author
@@all = [ ] def initialize (quotes_array)
quotes_array.each do |quote_hash|
quote_hash.each do |key, value|
self.send(("#{key}="), value)
end
@@all << self
end
end
# more code...
end

When I passed in the quotes_array of two hashes to the above initialize method, I got the following output:

Quote.new(quotes_array) =>#<Scraper::Quote:0x0000000002b35860
@author="Abraham Lincoln",
@body=
"Always bear in mind that your own resolution to success is more important than any other
one thing.">

As I expected, adding a second iteration accesses the keys and values from the hash, and a quote object, with Abraham Lincoln as the value for the author property, had been instantiated. Now, I was also expecting a quote object with Benjamin Franklin as an author to had been instantiated as well. So, I went on to examine my Quote @@all class variable (an array), and I found this:

Quote.all => [#<Scraper::Quote:0x0000000002b35860
@author="Abraham Lincoln",
@body=
"Always bear in mind that your own resolution to success is more important than any other
one thing.">,
#<Scraper::Quote:0x0000000002b35860
@author="Abraham Lincoln",
@body=
"Always bear in mind that your own resolution to success is more important than any other
one thing.">]

Uhh, only one quote object had been instantiated (with Abraham Lincoln as author) and had been added to the @@allarray twice. To better understand why a quote with Benjamin Franklin had not been instantiated, I reversed the order of the hashes from the quotes_array and made a reversed_quotes_array:

reversed_quotes_array =  [
{:body=>"Always bear in mind that your own resolution to success is more important than any other one thing.",
:author=>"Abraham Lincoln"},
{:body=>"I didn't fail the test. I just found 100 ways to do it wrong.",
:author=>"Benjamin Franklin"}
]

I cleared my @@all class variable, and passed in the reversed_quotes_array to my initialize method, and got the following result:

Quote.new(reversed_quotes_array) =>#<Scraper::Quote:0x0000000002a805c8
@author="Benjamin Franklin",
@body="I didn't fail the test. I just found 100 ways to do it wrong.">

Now a quote object with Benjamin Franklin as an author had been instantiated. Again, I went on to examine my @@all array, and found the following:

Quote.all =>[#<Scraper::Quote:0x0000000002a805c8
@author="Benjamin Franklin",
@body="I didn't fail the test. I just found 100 ways to do it wrong.">,
#<Scraper::Quote:0x0000000002a805c8
@author="Benjamin Franklin",
@body="I didn't fail the test. I just found 100 ways to do it wrong.">]

Uhh, only one quote object had been instantiated, but now from the hash containing Benjamin Franklin as an author, and had been added to the @@allarray twice. In both cases, namely, the quotes_array and the reversed_quotes_array, only the last element in the array had been used in the initialize method to instantiate exactly one new quote object. Therefore, I found that

“Passing an array of hashes as an argument to the initialize method will instantiate exactly one object from the last hash in the array only.”

And so, here is the learning:

“When the initialize method is called, it will instantiate exactly one new object for that specific call.“

Consequently, in order to instantiate many objects at a time, we need to call the initialize method many times. Hence, instead of iterating twice within the initialize method to access the keys and values from the iterated hash, we will iterate over the array of hashes (the quotes_array) in such a way that one hash at a time is passed as an argument to the initialize method. This can be accomplished with the below code:

class Quote
attr_accessor :body, :author
@@all = [ ] def initialize(quote_hash)
quote_hash.each do |key, value|
self.send(("#{key}="), value)
end
@@all << self
end
def self.create_from_list(quotes_array)
quotes_array.each {|quote_hash| self.new(quote_hash)}
end
# more code...
end

The .create_from_list class method takes in an array of hashes as an argument, then it iterates over the array and passes one hash at a time as an argument to the initialize method, this later encapsulated within the self.new(quote_hash) method. When we pass our quotes_array of two hashes (the first hash containing Benjamin Franklin and the second hash containing Abraham Lincoln) to the self.create_from_list method, and examine the @@all class variable, we obtain the following results:

Quote.create_from_list(quotes_array)Quote.all =>[#<Scraper::Quote:0x0000000002a80528
@author="Benjamin Franklin",
@body="I didn't fail the test. I just found 100 ways to do it wrong.">,
#<Scraper::Quote:0x0000000002a80460
@author="Abraham Lincoln",
@body=
"Always bear in mind that your own resolution to success is more important than any other
one thing.">]

Looking at the results, I accomplished what I wanted: two quote objects were initialized, corresponding to the two hashes in the quotes_array. This way, we can have 100, 1000, or more hashes in our quotes_array, and a corresponding number of quote objects will be instantiated. This is called Mass Assignment.

Loading Databases and Code Refactoring

The main display of my command-line-interface (CLI) had the following structure:

I wanted to have my databases loaded and my class objects already instantiated before the user entered his/her first input (more specifically, my first level scraping databases and corresponding class objects). So I created a method called load_databases that would load my databases and instantiate my class objects via mass assignment. The load_databases is the first line of code that is executed in my CLI class, as shown in line 127 in the below picture (picture shows only a section of the CLI class):

The intro_display method (on line 128) is just a puts method for the words read in the first picture for this section. When the user enters 2, the user sees the following:

Finally, when the user picks a category, a quote from the selected category is displayed:

Now, assuming that the user enters 2 to go back to the main menu, then enters 2 to go to the “Quote from category” section again to get a quote from the listed categories, the user would see the following:

Uhh? What happened? Instead of displaying the original 5 categories, now 10 categories are displayed: the 5 original categories plus a duplicate of the 5 originals . Why is this? Well, the last method within option 2 in my main_menu_method (on line 140 from picture 2) will call on self.call (that is, QuotesApp::CLI.call) to return to the main menu; however, calling on self.call invokes the load_databases method, and so, a new set of objects for each of my classes are created and added to the classes’ @@all array; more specifically, duplicate objects are created for each class. In fact, every time the user decides to go back to the main menu would cause a new set of duplicates to be created. This is terrible behavior! How could I load my databases and create my class objects just the first time the CLI is called? I refactored and came up with the below solution:

I defined a run instance method (on line 126) that would first, load my databases and instantiate my class objects, and then call on the call method responsible for displaying information and handling user input. The solution? No user input would ever invoke my load_databases method, and the method would be called only once: when the CLI is first opened.

I’ll be looking forward to learning more Ruby.

--

--