If you have built a Rails application, there is a good chance the power of Ruby gems were utilized to quickly add new functionality. In this article, we're going to go over some best practices to follow when using gems within a Rails application. I will demonstrate how needed gems can be specified in your environment, unpack a copy into your application and then we will do a little gem hacking without breaking them.
The application we are about to build won't really be an application, we won't be creating any views, controller or even touch the database. We will be using the Gruff gem by Geoffrey Grosenbach to generate some simple graphs and then we will enhance Gruff with some new options.
Installing and Freezing
Let's get started by creating a new Rails application and installing the Gruff gem.
rails gem_hacks sudo gem install gruff
Add the following in the Rails::Initializer.run block in your config/environment.rb:
config.gem "gruff"
By adding config.gem "gruff" in our environment the application will look for the gruff gem on start up. If it doesn't exist it will throw an exception. This is much better than having the application running while the portions that depend on the gem error out.
Lets make sure Gruff appears in the application's gem list and then copy it to the vendor/gems directory of the application with the unpack task.
rake gems rake gems:unpack:dependencies gruff
By including Gruff within the application, we won't have to install the gem on every box the application finds itself on. Also, every developer working on the project will not have to worry about having the same gem version installed.
Making It Our Own
Next let's create a BarGraph class that will require 'gruff'. The BarGraph class will serve as a wrapper method for generating common bar graphs found on the site.
require 'gruff' class BarGraph end
Every ruby class needs an initialize method in order to create new instances. Add an initialize method inside the BarGraph class filled with Gruff code that will generate a static bar graph for now. Then create the bar_graphs directory inside of public/images.
def initialize g = Gruff::Bar.new("400x300") g.data :years, [185, 155, 110, 90, 135] g.labels = { 0 => "2004", 1 => "2005", 2 => "2006", 3 => "2007", 4 => "2008" } g.write('public/images/bar_graphs/test.jpg') end
Next fire up your console and try BarGraph.new, then go into your public/images/bar_graphs directory and open up test.jpg, it should look like the following:
Making Improvements
Notice how the bar of the lowest value (90) is barely visible? By default, the minimum value will be the lowest value and the maximum will be the highest value. We can customize this by setting more attribute values of the instance. Add the following before the g.write line.
g.minimum_value = 0 g.maximum_value = 200
Let's once again regenerate our graph through the console; now the bar with the value of 110 should be visible.
Great, our graph starts at 0 and we've added some potential by specifying a higher maximum value. Lets make our BarGraph class more dynamic by adding some arguments that can be passed through the initialize method and refactor the code within.
def initialize(values, labels, save_path, options={}) g = Gruff::Bar.new("400x300") g.data :years, values g.labels = labels g.minimum_value = 0 g.maximum_value = 150 g.write(save_path) end
Now whenever we call BarGraph.new, arguments for values, labels and save_path will be needed. Reload your console and give the following a try:
This will generate the same graph as before since we are passing in the same data. If we passed in an array with values higher then 200, however, it would get cut off due to the static maximum value in our initialize method. We can remove the maximum value portion from our initialize and Gruff will default to the highest value in our array. Another option is to add code that will take that maximum value and round it up.
Hacking the Gruff Gem
The auto-rounding of the maximum value would be a good feature to have in other Gruff graphs as well. Since we have the gem source in the vendor/gems of our projects, adding it there might sound like a good idea at first. Later in the project lifecycle, however, the gem might be updated and those custom additions would be lost.
The ideal solution would be to create a new module that would contain our hacks which will be included wherever needed. Whenever the gem is updated and our hacks break, we can easily fix them since they are all located in one place. With that being said let's create our GruffHacks module and save it as gruff_hacks.rb in lib.
module GruffHacks class Gruff::Base end end
Our GruffHacks module contains the Gruff::Base class which is found in vendor/gems/gruff/lib/gruff/base.rb. Take a few minutes to browse through the Base class as we will be overriding methods from it next. Near the top all the attribute accessors for Gruff are being set. These attributes are shared between all graphs since they are part of base. Lets add a attribute accessor for rounding maximum values called round_maximum_value into Gruff::Base of our GruffHacks module.
Remember we are not editing any code from the gem source, we are only going to use it for reference and copy methods that we will be overriding from it. Next lets copy the entire initialize_ivars method from the gem source into Gruff::Base of our GruffHacks module. At the bottom, after @norm_data = nil, let's add @round_maximum_value = false. The initialize_ivars method is responsible for setting default values for Gruff attributes, here we are setting @round_max_value to false to cancel out the rounding functionality we will add soon. The goal with this module is to enhance Gruff, but to have it function normally if our attribute hacks are not being set.
With the round_maximum_value attribute accessor along with its default value added, now it is time to set the new maximum value when round_maximum_value is true. Add the following method into Gruff::Base of our GruffHacks module
def round_maximum_value=(value) if value == true rounded_maximum_values = [] # loop through each bar group, grab the highest value, round it up and add # to rounded_maximum_values array highest_val = @data.each do |row| row_highest = row[1].sort.last round_to = 10 ** (row_highest.to_s.length - 1) rounded_maximum_values << row_highest.roundup(round_to) end # Set maximum value based on highest rounded value @maximum_value = rounded_maximum_values.sort.last end end
Add this after the very last method of GruffHacks, these two helpful methods are created by Charlie from PullMonkey for rounding numbers up or down. Our round_maximum_value method uses the roundup method.
class Numeric def roundup(nearest=10) self % nearest == 0 ? self : self + nearest - (self % nearest) end def rounddown(nearest=10) self % nearest == 0 ? self : self - (self % nearest) end end
Before testing our new hack, lets include the GruffHacks module into our BarGraph class by adding include GruffHacks after the Gruff require statement. In our initialize method remove the g.maximum_value line and add in g.round_maximum_value = true. If you try regenerating the same graph as before, the maximum value will now be 200 since 185 was rounded up.
Worth the Effort
By overriding the initialize_ivars and adding the round_maximum_value setter into Gruff::Base we are able to automatically add some space between the highest value and the top of the graph by simply setting a single attribute to true. If there is a need to change this functionality later on, we won't have to dig through the gem source and hunt for modified lines. Instead we open up our GruffHacks module and easily find our overridden methods. It is also much easier to add new methods, maybe more style options are needed for the bars or the labels need to be positioned in a different way. Whatever it is, by keeping these enhancements in a separate module makes sharing and updating hacks between applications easier.
As projects grow, it becomes harder to keep up with all the details. Even if you are the only developer on the project, there is a good chance the inner workings will be forgotten months later. By breaking simple hacks into new modules we are minimizing the potential of possible bugs, and any bugs that do exist will be easier to find. Spending a few extra minutes to better organize your code and using the object oriented nature of a language such as Ruby will save time for you and your fellow colleagues.

