Lesson Weekend

Sometimes it’s not always clear where our code should go in a Rails application. For instance, let’s say we’re building an application that shows the best hikes in the area. As we’re building functionality we decide users should know the weather forecast while looking up a hike, which means we’ll need to make a call to a weather API. But where does the code to make an API call go?

API Call Locations

If the results of the weather API will only be displayed on the show or index route of hike_controller.rb, we could put the call in the controller.

However, while this works, it's actually a bad idea. Controllers should be as lean as possible. Back-end logic generally belongs in the model, not the controller.

So it seems the logical place for this code is our hike.rb model. We could create a check_weather() method in hike.rb, then call the method in our controller, right?

Actually, no. While this would work, and while it's still better than putting code in the controller, there’s still a problem; the weather has nothing to do with our hike.rb model! We need to separate our concerns and keep our code as clean as possible.

So what’s the next option? We could create a new model called weather.rb and put our code there. Wouldn’t that be a good solution? Maybe, but only if we actually needed to access a weather table in the database (such as to save or retrieve information for the API call). If we take a look at any model in a Rails application, we can see it inherits from ActiveRecord::Base. In other words, Rails models are designed to use ActiveRecord and communicate with the database.

For the purpose of our application, let’s say users will enter their preferred hike and the weather forecast will be returned. There’s no need to save any weather information to the database. As a result, we don’t actually need a model!

Plain Old Ruby Objects

In this case, we should create a plain old Ruby object (or PORO). We'll create a new Weather class and call the method there. To simplify our code example, we’ll return only the humidity for now:

models/weather.rb
class Weather

  def initialize(zip)
    @zip = zip
  end

  def get_humidity
    response = HTTParty.get('http://api.openweathermap.org/data/2.5/weather?zip=' + @zip + ',us&appid=[YOUR API KEY HERE]')
    response["main"]["humidity"]
  end
end

This code is just a plain old Ruby object. In this case, we can initialize an instance of the Weather object with a zip code (we’ll add corresponding code to our controller soon) and then call our get_humidity() method on the instantiated object in the controller.

Note on Gems: This example uses the HTTParty gem, which provides similar functionality as the REST Client gem. Both are recommended. As always, you’ll need to add the gem to your Gemfile, then bundle.

You’ll also want to store your API key in an environmental variable using the dotenv gem (you can also try the Figaro gem if you’d like to experiment).

Now let’s add code to our controller so we can use our get_humidity() method:

controllers/hikes_controller.rb
  def index
    weather_object = Weather.new("97210")
    @humidity = weather_object.get_humidity()
  end

Here we’ve instantiated a new Weather object in the controller, then called get_humidity() on it. The zip code is hard-coded for now, but we could easily change this to grab parameters provided by the user. We’ve also made @humidity an instance variable so it is available in our index.html.erb view.

Our application now includes a functional PORO. But there are still two issues:

  • First, our PORO is in app/models even though it’s not a model. Imagine a large application with many models and POROs in the same folder. It would be a nightmare to keep them organized.

  • Second, our PORO is vaguely named. For now, our PORO only needs to do one thing: get the humidity. However, it’s called weather.rb. Sure, it’s the name of the class, but it doesn’t do a good job illustrating the purpose of our PORO.

Placing PORO Files

Let’s start with our first problem: our weary PORO needs a home. We have a number of options, each of which could be appropriate depending on our particular use case or preferences.

  1. We could create a folder called app/controllers/hikes_controller and put the PORO in there. If the PORO will only be accessed in hikes_controller.rb, this namespacing will help organize POROs only used in this controller.

  2. We could also put our code in the lib folder. lib is generally where we store pieces of code that can be reused across multiple applications. It’s feasible that we could use this code to retrieve the weather in other applications, too.

  3. However, we’ll go with door number 3. We’ll create a new directory called app/services and place our PORO there.

A service provides a piece of functionality that can be used in other application components (in our case, the controllers). While including services isn’t an intrinsic feature of Rails, it’s an important design principle that’s popular with many developers and frameworks (such as Angular).

Naming PORO Files

Now that our PORO has a home, let's address the second problem we identified. We'll give it a new name. weather.rb is vague and doesn’t describe what our service actually does. Since our service just retrieves the humidity for now, we’ll call it find_humidity.rb. This will make it easier for others to quickly understand our code.

Eager Loading

There’s one more problem. Once we move our PORO to app/services, our application can no longer access it in our development or test environments. We’ll get the following error if we try to load the route:

uninitialized constant SomeController::Weather

In production, Rails 5 is set to eager load all directories in the app folder, but this is set to false for testing and development. Eager loading is the process of loading all resources immediately, as opposed to only when they are needed (which is called lazy loading).

We can fix this issue in our Rails application by changing the line

config/environments/development.rb
...
config.eager_load = false
...

to

config/environments/development.rb
...
config.eager_load = true
...

To test your code, you'll need to do the same for (config/environments/test.rb).

Note that the lib folder is not eager loaded in Rails 5, either. One solution to this issue (if you do choose to use the lib folder) is to move lib to the app directory so it’s also eager loaded.

You can check which paths are being loaded in your application by running the following in the command line:

$ bin/rails r 'puts ActiveSupport::Dependencies.autoload_paths'.

Now that you know how to make POROs and where to include them, get creative and figure out ways to incorporate them into your own applications. For instance, in the last lesson, we created an API call for getting the latest bestsellers from the New York Times. Because this information doesn't need to be persisted to the database, we should use a PORO to make all call-related logic and put this PORO in a services folder.

Terminology


  • Plain Old Ruby Object or PORO: A basic Ruby object used in a Rails application that is not backed by ActiveRecord, or stored in the database.

  • Service: A type of file that provides a piece of functionality that can be used in other components of an application. Services aren't an intrinsic feature of Rails, but they're an important design principle popular with many developers and frameworks (such as Angular).

  • Eager Loading: The act of loading all directories and files immediately, as opposed to only when they're needed (which, conversely, is referred to as "lazy loading").

Tips


  • Back-end logic generally belongs in the model, not the controller.

  • Service file names should be descriptive of what the service actually does.

  • You can check which paths are being loaded in your application by putting the following in the command line:

$ bin/rails r 'puts ActiveSupport::Dependencies.autoload_paths'.

Additional Resources