Refactoring My Previous CLI Code

With recent events that have unfolded during my life while coding, I’ve been stuck in a loop where I do much less coding than I desired. I finally overcame many problems and sat down to think on how to make my original CLI code much better. I’ve received some ideas with load time and writing cleaner code. My Original code called “cli.rb” looked something this:

As you can already tell, it seems to be very cluttered, over 97 lines of code just for a few features and not so many dogs. It gets a little less large but more pattern matching with the next set of code I had to refactor. But to clarify what’s going on within my code, I started with the call method, which gives the user a different method that includes a string “List of Dogs”; it also lists all the dogs numbered 1–4 for the user to type in a number to choose a dog.

A Bernese Mountain Dog

Now for the really fun part, the menu method I included within the call method. This is where the magic happens between the user and the Command Line Interface. I start with having an input = to nil, using the while loop set to ‘input != “exit”’ I have it looping a string that asks the user to enter a number 1–4 to get a dog bio loaded up for them, if they cannot remember the list, they are able to type in ‘list’ to see the list again and finally ‘exit’ if they wish to stop the program. Using the input with a bang and having it set to a string “exit” I give the power to the user to end the program when they choose. I also had to include “input = gets.chomp.downcase” in order to have the user not have trouble with errors with capitalization anywhere i.e. “Pug” wouldn't work if I did not include downcase, it would give the user an error. So instead I have the program automatically lowercase all input strings so there would be no mistakes.

Going in deeper into the menu method you’ll see that theres an if statement set to ‘input.to_i > 0’ and having “doggo” set to a class variable “@dogs[input.to_i-1]” and having it put out “#{doggo.breed}”, What this does is that it scrapes from a website we’ll get into later, then prints a string for the user with the information they wanted. The reason that I have input.to_i-1 is because programs start at 0, while us humans start at 1; to help the program understand what the user wants and what the program can understand I have the user type in 1, which the program will understand as 0. With that the user will receive the information they wanted without having to compromise with the program and type “0” for something listed as “1”. You’ll also notice that I also inserted elsif statements for each input that the user decides to type in the breed they would like to choose. If they were to put in any other string I have the else statement for them to check their spelling before entering or if they forgot the choices they could type ‘list’ again.

Realizing the amount of code there really was before refactoring

The scraper file I created was made to be a little better but it was still taking a while to load sometimes. This is the “dog_scraper.rb”:

Once again there’s a bunch of code and pattern matching, as someone on their way to becoming a junior web developer, it looks fine to me. But I had someone with much more experience than me look over it and they were questioning why there was so much more code than there should be, note; that it works, it just seems theres more code than there needs to be.

Realizing how much unnecessary code there is.

With that, This class has me scraping from the Purina website, then making sure i’m taking the correct data using nokogiri. I repeat the process for the other dog bios and then put them within an array and have a method call upon that array in a list format.

Looking upon what i’ve written down, It seemed a little messy, a ton of pattern matching and just too much code at once. So one of the first questions I’ve asked was “what could make this better?” So after a few minutes of googling, a couple of hours of trial and error, I finally reached out for help through a forum and a more experienced programmer. Here is what the final product looks like for each file, with brief explanations of what it does.

First thing that had to be done was creating a class for my dog bios. With it I had to make sure it had an empty array along with initializing the url and breed. This is one of the back bones of the program working smoothly. This will create new dogs on the fly which will be explained more later on and makes things a whole lot easier to understand. I also have had the DogBio class name spaced with the module Dog. Now for what is in the Dog module…

Dog Module

This is what has the files be able to read and interact each other. Which means I could call upon other methods from other classes to use in other files or classes, Along with being able to use pry, nokogiri, open-uri in any file with no hassle. Below is the Dogbio Class.

dog_bio.rb

Creating the DogScraper class I first had to make sure it was name spaced with the module Dog. I then wrote down an accessor that takes breed and bio. As explained above, the dog module will help us use various methods from other classes and files.

Beginning of the DogScaper Class

Now here comes the meaty part of the class. Scraping the data from the Purina Website, then iterating through all the dogs I can and making sure I use my DogBio class to create new dogs. Sounds a little weird, so let’s dive in. First I had to go to the Purina Website of course and then see their breed list of dogs.Purina’s list of dogs was https://www.puroina.com/dogs/dog-breeds. So now within my code I had to add a class method and have Nokogiri namespace with HTML and open that website within my code.

Beginning of scrape_dogs

Now that thats done, we have to go even deeper to find the information that we need. I was using Google Chrome when I was on Purina’s Website, so in order for me to get the right information I needed for my CLI, I had to use the inspect button. To use this tool, you must right click anywhere on the page and click inspect. Once thats done, You’ll get something like this.

Inspect on Google Chrome

Looks like theres a lot of code. It would be very difficult to find what you need within it. Thankfully Chrome has a small tool that helps find specific things you’re looking for by hovering over something and it highlights the property on the page. It’s located towards the elements section near the top middle and the picture is a mouse hovering over a square. I’ve used this to help find the correct area of where it lists the dogs, which turned out to be div class= blocks div callout. So now I must add it to my code by having it searched using the previous code but now assigning to doc.

scrape_dogs now with a list

Not done just yet. We now have to iterate through each dog and make sure they come with their name and bio, but not just that, we also need them to be created within our code so they could be called upon. So this is where we finish the rest of the refactor. Instead of hardcoding certain dogs and then scraping their data, we now have the first page scraped which consists of 12 dogs and have them each iterated and put into text for the user. I assigned a variable by the name of dogs and had it use the doc.search line. I then used the iterator each to have them all listed. I wrote down 2 variables and assigned them both different properties. Breed used dog.search(“span.callout-label”).text to grab the html tag and then have it put into a string using text. I then assigned a variable by the name of url and had it set to dog.search(a.link).attr(“href”).value which grabbed the link to each dog within the iteration. After those were set, I had to create new dog objects. So I took from my old model and used the module Dog name spaced DogBio and created a new dog object using the new variables I just created.

Complete class method of scrape_dogs
Using pry to show the new dog object thats been created.

Now theres a slight problem. The link doesn't open Purina.com/dogs/dog-breeds. It only grabs the last bit of the address, not only that, it doesnt grab the bio for that dog and put it into text form for the user, to fix this I had to make another method. This time I called it self.scrape_dog with an argument of dog_bio, which consist of what the user picks within the menu I made in the cli file, it has an instance of breed and url attached to that breed. I assign the beginning of the website “https://www.purina.com” and add the arument being dog_bio and take it’s url, in english, the program will now read the user input and then take the string “https://www.purina.com” and attach whatever dog’s url the user picked to it so it will have the whole address.

self.scrape_dog being added

Not only do we have to write the beginning of the web address but we also need to grab the entire bio from the dog website and assign it to a value as well. If you recall we had an accessor of bio in both dog_scraper class and dog_bio class, this helps us read and write the code and have it saved for later. So now is the time we assign another value being dog_bio.bio with doc.search(“section.mainContent p”).text, this is what contains the the dog bio paragraphs.

One last thing that’s missing that we’re going to need for later on. I should have another class method that holds another method’s code, to make it more simple and bring it over to another class file. So I added self.list and put the scrape_dogs method within it. It’s gonna be important for later. So the final look of the dog_scraper class looks like this.

Final Version of the DogScraper Class

It looks way better than before with less hard coding and pattern matching. It’s clean and simple to read for a lot more people. Cutting it down from around 47 lines of code to 24.

So far so good

Now for the CLI class. We start off by using the dog module once again and name spacing it with the name CLI. We don’t have to add any attr for this class seeing how we are taking from other files completely and just creating a menu and list. So we start off with an instance method and naming it call. Within the call method is what we’re gonna put all of our methods within, such as the menu and the list of dogs. But first we’re going to put in the scrape_dogs class method. With that we then have to make a method that will list dogs for the user to be able to read. We create a list_dogs method, now we have to iterate through an array of dogs we’ve scraped where thankfully we have that already built; it’s within the Dog_bio file, because of this we have to now have to call upon the file and have it display all dogs using Dog::DogBio.all, I then have it set to a class variable of dogs. Making sure it worked, I used pry and if done correctly with the code above, it should look something like this in terminal.

Terminal displaying the array of dogs

Now we must iterate through all the dogs and have them display their breed once called upon and also have them all set to a number so that when a user picks that number, the breed will be displayed. So we write out our class variable and using each with index of 1, we can grab each dog and have it set with number, we then have the iteration print out a string with the integer thats attached to each different dog and dog breed with a line break so that the user will not be confused reading all lines with no line breaks and be confused reading. Our list dogs method is now ready and should look something like this!

Whats next would be our menu for the user, we would need something that’s easy and accessible for just about anyone to be able to use. So I first start off calling the instance method menu, I have the input set to nil and use the while loop to where it ends when I have someone type in exit, if they do not they will receive a message asking them which dogs they would like to see. Now we have to give an option to the user, we create a variable by the name input within our loop and set it to get.chomp.downcase so that the user has the ability to type something in. Not only that, but if they Capitalize the “E” in “Exit” they won’t get stuck in the loop and not know why. Chomp is used to get rid of the trailing new line character that the program attaches to each end of a string. I also wrapped the class variable of dogs that holds all the dogs and have it set to length so it would count how many dogs are within the array and so I don’t have to keep updating when I decide to add more dogs to the program. So far it will look like this.

The tricky part of the menu is grabbing the specific dog the user wants to know more about. In order to this we must now use an if statement and have it set to certain conditions so that we can grab the right dogs for the user. I put in the if statement with the variable of input.to_i > 0 which means that when the user puts in a number instead of it coming out as a string, it will be registered as an integer and having it set to 0, the user will will have the program run if they put in an integer higher than 0. I also had to write out a condition that the user had to must put in an integer less than or equal to the class method of dogs.length, this will come hand later on when I use the else statement. Right now the code is looking pretty good.

We now have to finish the if statement, add some elsif’s, then finally give an else at the very end. Finishing up on the if statement I decided to have create a variable within the method’s scope called doggo that takes in the class method of dogs thats attached with the input from the user set to an integer but have it subtracted by 1 because computers start their count at 0 when counting from an array or most things while humans start counting at 1. I then print out a string with puts that calls upon the variable and takes the class method and prints out the breed only using .breed along with a line break at the end, this will print out the dog’s name. I then need the dog’s bio, so once again I call upon the class called DogScraper.scrape_dog giving it an argument of doggo once again and having set to unless doggo.bio then printing out the dog bio itself with a line break. The unless part helps with the scraping, once a user tries to pick the same dog once again the program will not scrape continuously but will post the same dog they asked before making everything more efficient.

Update Menu

For the final parts of the menu, i used the elsif statements within the if statement to be able to give the user the ability to call upon a list with the method we’ve created before with “list_dogs”. I also created another method called goodbye and had it print out a goodbye message for the user, nothing too crazy, it’s just a string, I had it set to exit so that when a user wants to end the program they get a tiny message. Finally for the reason why I added “&& input.to_i ≤= @dogs.length” was for the else statement that prints out “input invalid…” and gives similar options from list to help the user, instead of the program not knowing whats going on and crashing.

final menu

We finally have our CLI class finished!

Thank you for taking the time to read all the code refactoring and seeing how I made my Dog Bio classes! I really appreciated it!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store