Where Does Your Code Go?

After you finish the Rails tutorials and start your own app, things get confusing. Like where does your non-CRUD, general logic go? How does grabbing followers from Twitter fit into MVC? Ask two people, and you get four answers. Or your thread devolves into a bunch of smart people insulting each other for hours, while you beat your head against the desk. Either way, you’re left with a headache.

You can’t build the app you’ve been dreaming of without some general, non-Rails logic. So where do you put your code, and still keep things simple?

The easy place to start

When I have logic that feels related to an existing ActiveRecord model, I’ll start by putting it into that model. For example, if I had a Game model and I wanted to import a bunch of games from CSV files, I’d put that method right onto the Game class:

app/models/game.rb
class Game < ActiveRecord::Base
  def self.parse_from_csv(csv_string)
    games = []
    CSV.parse(csv_string, quote_char: "'") do |row|
      games << Game.from_csv_row(row) if (row[0] == 'G')
    end
    games
  end

  def self.from_csv_row(row)
    Game.new({
      dgs_game_id: row[1],
      opponent_name: row[2],
      created_at: row[4],
      updated_at: row[4],
    })
  end
end

You have all the information your methods need right at hand. It’s easily testable. And it’s probably where a new contributor would look for that logic first.

But if you keep adding and changing that model, it’ll get big and complicated. Different parts of the model will interact in strange ways. The more you change it, the harder it will be to change.

In that case, you’d probably want to refactor that code out to a non-ActiveRecord model.

Non-ActiveRecord models

Just because it’s in a Rails app, doesn’t mean it has to inherit from Active/Action-whatever.

You can write your own Ruby code, in plain Ruby objects, and use them in your Rails app. These objects can still be called models, because they’re modeling part of your problem. They just don’t have an ActiveRecord database storing their data.

The next time I worked on that game CSV parser, the Game class was getting a little too big. So I moved the parser logic into its own GameCSVParser class.

The whole commit is here, but this is what the non-ActiveRecord class looks like:

app/models/game_csv_parser.rb
class GameCSVParser

  def initialize(csv_string)
    @rows = CSV.parse(csv_string, quote_char: "'")
  end

  def games
    game_rows.map { |row| Game.new(game_attributes(row)) }
  end

  private

  def game_rows
    @rows.select { |row| is_game_row?(row) }
  end

  def game_attributes(row)
    {
      dgs_game_id: row[1],
      opponent_name: row[2],
      created_at: row[4],
      updated_at: row[4],
    }
  end

  def is_game_row?(row)
    row[0] == 'G'
  end
end

I’ll go right to creating a new plain Ruby object if the logic I’m adding doesn’t feel related to any specific ActiveRecord model. Or if the code seems like it should be a part of a thing that doesn’t exist yet in the app. Otherwise, they mostly pop up through refactoring.

With plain Ruby objects, you can write anything. But knowing you can write anything doesn’t help you with direction. What methods do you need? How will all your new objects interact?

A lot of Rails apps use the same categories of plain Ruby objects. These categories are patterns you can follow to write code that other developers would recognize. You might have heard of a few of them already.

Service objects, presenters, and jobs

There’s nothing special about service objects, presenters, and jobs. They’re just plain Ruby objects that act in a particular recognizable way.

For example, a Resque job is a plain Ruby class that has a perform method and a @queue:

app/workers/fetch_games_for_player.rb
class FetchGamesForPlayer
  @queue = :default

  def self.perform(player_id)
    player = Player.scoped_by_id(player_id).ready_for_fetching.first
    player && player.fetch_new_games!
  end
end

perform is called when the job is run.

A presenter is a plain Ruby object with code that only makes sense inside a view:

app/presenters/user_presenter.rb
class UserPresenter
  def show_related_users?
    @user.related.count > 3
  end
end

It might also include Rails’ view helpers, or take a few different objects and treat them as one unified object for the view’s convenience.

A service object is a plain Ruby object that represents a process you want to perform. For example, writing a comment on a post could:

  1. Leave the comment.
  2. Send a notification mail to the author of the post.

A service object could do both, and keep that logic out of your controller.

There’s a great take on service objects here. It’s full of examples.

For simple processes, I don’t bother with service objects. But if the controller starts getting too heavy, they’re a good place to put that extra logic.

You can use these patterns to organize your own business logic. They’re just plain Ruby objects, but they’re Ruby objects that share a certain flavor, that have a name, and that you can talk to other developers about.

Where do you start?

There are a lot of different places your non-Rails business logic could go. It might be hard to choose. So here’s what I do:

  1. If the logic is mostly related to an existing class, even if it’s an ActiveRecord model, I put it in that class.
  2. If it doesn’t fit an existing class, I create a new plain Ruby class to hold the logic.
  3. If it feels like the logic should be part of something that doesn’t exist yet, I create a new plain Ruby class for it.
  4. If I come back to the code later, and the model is getting too complicated, or the code doesn’t make sense in that model anymore, I’ll refactor it into its own plain Ruby class.
  5. If the code only makes sense in a view, I’ll add it to a helper or create a presenter.
  6. If the code doesn’t need to run during an HTTP request, or has to run in the background, it goes in a job.
  7. If I’m juggling several different models or stages of a process, and it’s making the controller too hard to understand, I’ll put it into a service object.

How about you? Where does your code go? And do you have any patterns besides these that you’ve found helpful? Leave a comment and let me know.

And if you don’t have a process yet, try mine. See how it fits you. There’s no perfect way to write code, but when you get stuck, a process like this will help you get started.

Pushing through tutorials, and still not learning anything?

Have you slogged through the same guide three times and still don't know how to build a real app?

In this free 7-day Rails course, you'll learn specific steps to start your own Rails apps — without giving up, and without being overwhelmed.

You'll also discover the fastest way to learn new Rails features with your 32-page sample of Practicing Rails: Learn Rails Without Being Overwhelmed.

Sign up below to get started:

Powered by ConvertKit

Did you like this article? You should read these:

Comments