CLI gem project: optimising and refactoring

Posted by Alice Brunel on August 31, 2019

I “finished” my project around 7 days before the project delivery due date: the application was working, the navigation was defined, the text of each interface was carefully worded.

Introduction

I made an application who could scrape data from the PocketCamp wiki webpage and display these information in a CLI. PocketCamp is a mobile game in which you can built a little campsite and invite friendly animals in. These animals can sometimes give treats, such as building resources or what we call “essences” - basically a different kind of resource that make the game a little bit more difficult. You can find the code at this address: Pocketcamp_guide_cli

My CLI application is structured as below:

  • with a Scraper class scraping data from the wiki webpage
  • with an Animals class creating Animals objects from the data scraped by the Scraper class
  • with a CLI class calling both classes to create the CLI application and to organise the navigation and UX

The application is based around 4 functionalities:

  • It could print out a list of all the animal names
  • It could retrieve animals from a search by name to get information about an animal
  • It could retrieve animals from a search by resource to know whose animal can provide a specific resource
  • It could retrieve animals from a search by theme to know whose animal can provide a specific essence

First commits

At first, I created the main navigation interface, and pseudo-coded the result I was aiming for: First, the text

def first_choices
	  puts "What would you like to do?"
	  puts ""
	  puts "To view the list of animals name type 0"
		puts "To search by animal name type 1"
		puts "To search by resource type 2"
		puts "To search by essence type 3"
		puts "To exit type exit"
		puts ""

Then for each option, I imagined a search method:

		input = gets.strip
		if input == "0"
		  print_all_animals
		  first_choices
		elsif input == "1" 
		  search_by_name
		elsif input == "2" 
		  search_by_resource
		elsif input == "3"
		  search_by_essence
		elsif input.downcase == "exit"
		  puts "See you on the campsite!"
		else 
		  puts "This input is not valid"
		  first_choices
		end

First methods: search_by_name and find_by_name

I naturally started with the find by name method. I needed the app to find the object whose name was equal to the input given by the user and to get the object in return. I ended up with this method:

  def self.find_by_name(input)
    @@all.find {|animal| animal.name == input}
  end

@@all being a class variable storing all the Animals object in an array, I used the find method to find in each animal object wich animal object had an instance variable called “name” that was equal to the input given by the user. This method was working well and was returning the object each time it found and input equal to the object name.

In the CLI class, I then only had to use the returned object in another method which could display the object information:

	def display_animal(animal_object)
	  puts "---|#{animal_object.name}|-----------"
	  puts ""
	  puts "   species: #{animal_object.species}"
	  puts "   It's theme and essence: #{animal_object.theme}"
	  puts "   This animal will give you: #{animal_object.resource}"
	  puts ""
	end

And created a search method taking in a user input and using it to display the result with the display_animal method:

  def search_by_name
    puts "Type a name or type back to return to the first choices"
    input = gets.strip.to_s.capitalize
    result = PocketcampGuideCli::Animals.find_by_name(input)
    if result != nil && input != "Back"
      display_animals(result)
      search_animal
    elsif input == "Back"
      first_choices
    else
      puts "Doesn't ring a bell... Try something else?"
      first_choices
    end
  end

This first attempt was working and made to return and display only one object.

Next methods: search_by_resource and find_by_resource

I started then to work on the next method. To search by resource, I needed the application to find all the possible resources in order to list them, then print them out to show them to the user and help her select a resource, then to display the multiple results based of her search. It was a bit trickier in the sense that I had to create more that 2 methods, unlike what I previously did:

  • I had to create a find method in the Animals class
  • I had to create a search method in the CLI class
  • I had to create a method listing the existing resources from the Animal instances
  • I had to create a method displaying multiple animals with the CLI method display_animal

So, this is what I did. The code looked like that:

# In the Animal class, a find_by_resource to compare and base the search on the user input
  def self.find_by_resource(input)
    @@all.select {|animal| animal.resource.include?(input) == true}
  end
	
#In the CLI class, a search_by_resource using find_by_resource and the user input
def search_resource 
    PocketcampGuideCli::Animals.get_all_resources
    input = gets.strip.to_s.downcase
    array_result = PocketcampGuideCli::Animals.find_by_resource(input)
    display_search_results(array_result)
end

#In the Animals class, a get_all_resources, putting all the "resource" instance variable in an array

def self.get_all_resources
    array = []
    @@all.each do |animal|
      array << animal.resource
    end
    puts "Type the name of the item you are looking for from the list below:"
    puts "#{array.uniq}"
  end
	
#And a method to display multiple results
def multiple_result(array)
    array.each do |item|
      display_animals(item)
    end
end

This was working, so I started building the last method.

Last method: search_by_essence and a refactoring opportunity

It was obvious that this last method was absolutely similar with the previous one. By I didn’t want to lose track of what I was doing and willing to do, so I built the same serie of methods for the “essence” search.

Once this was done, I took a step back and looked at all the methods to see where was the refactoring opportunities, in terms of technical implementation but also in terms of a UX optimisation:

  • The search by name was allowing the user to receive only one result. But first, it was possible that the user would be looking for a character without knowing exactly the spelling of its name. So it would be an opportunity to have several results to widen the functionalities of the application. If the application can return several relevant results, it would make the application more accurate.
  • The search by name could be based exactly like the other search methods. In this case, the application would be working around one unique find_by and search_by method which would make the code easier to read or to modify later.
  • I wanted my application to follow the Don’t Repeat Yourself precepts.

Then I had to find and describe the obstacle I created with my previous implementations:

  • The search_by_name method, in addition with the single result issue, was capitalising the user input to make is correspond to the instance variable (names’ first letter is always capitalise), while the other search methods were downcased.
  • The search_by_name method didn’t required to print a list of the possible choices, while this was the case with the other search methods (get_all_resources and get_all_essences).
  • To avoid hard coding the search criteria (“name”, “resource” or “essence”), I had to find a way to pass the criteria into the find_by and search_by methods.

After refactoring

Here are the methods that resulted:

#In Animals cass, I created a find_by method using the search criteria and the input to compare them and return an array of objects in result:

def self.find_by(criteria, input)
	  @@all.select {|animal| animal.send(criteria).downcase.include?(input) == true}
	end
	
#The get_all_lists prints out the possibilities (all names, all resources or all essences). 

 def self.get_all_lists(criteria)
    array = []
    @@all.each do |animal|
      array << animal.send(criteria)
    end
    puts "Type the name of the item you are looking for from the list below:"
    puts "#{array.uniq}"
  end
	
#In CLI class, I created a search_by method using the search criteria, and a printall parameter to tell the app whether it should print a list of possibilities or not - please notice that the names of the functions have changed for a better lisibility:

 def search_by(criteria, printall)
    search_criteria = criteria
		# Although it could, I told the app not to display this list for object name research
    if printall == true
    PocketcampGuideCli::Animals.get_all_lists(search_criteria)
    end
    input = gets.strip.to_s.downcase
    array_result = PocketcampGuideCli::Animals.find_by(search_criteria, input)
    display_search_results(array_result)
  end

#Finally, I rewrote the first_choices method
	def first_choices
	  puts "What would you like to do?"
	  puts ""
	  puts "To view the list of animals name type 0"
		puts "To search by animal name type 1"
		puts "To search by resource type 2"
		puts "To search by essence type 3"
		puts "To exit type exit"
		puts ""
		
		input = gets.strip
		if input == "0"
		  print_all_animals
		  first_choices
		elsif input == "1" 
		  puts "Please type the name of the animal you are looking for, then press enter:"
		  search_by("name", false)
		elsif input == "2" 
		  search_by("resource", true)
		elsif input == "3"
		  search_by("theme", true)
		elsif input.downcase == "exit"
		  puts "See you on the campsite!"
		else 
		  puts "This input is not valid"
		  first_choices
		end
	end
	

Conclusion

This implementation and refactoring improved the app by avoiding the use of 6 repetitive methods. It also made the logic more self explainatory and allows further modification and improvement of its functionality. This refactoring actually didn’t take much time.