Legacy Concerns in Rails

The cats out of the bag, Ruby isn’t immune to legacy code problems. Just because your app is written in a hip, fun, and dynamic language doesn’t mean that your codebase can’t stagnate, bloat, and quickly turn into an unmaintainable ball of mud. Before Gowalla was purchased by Facebook, the Rails code base stood at close to seven thousand files, with the largest model clocking in at around 3,500 lines of code. While we were somewhat unique, being originally written in Merb and then ported to Rails, applications of this size aren’t all that uncommon. If you’ve got a large app there are a number of things you can do make your situation better, one of the simplest with the greatest impact is splitting up models into concerns.

If you’re not familiar with concerns, you can read up about them at Concerned about Code Reuse?. Go ahead, we’ll wait.

Rails has long advocated a thin controller, fat model approach to development, which works great early but can lead to a model obesity epidemic. If we split out our models into different concerns we can group related code, and even make re-using code between projects easier. Best of all, if you start early on it’s a pretty painless process.

The User Model

There’s only two businesses that refer to their customers as ‘users’ and software is one of them. It’s also bound to be one of the largest models in your app since so much will likely need to be connected to a user. It’s a good place to start splitting up a model into a concern. Lets say that we want to add some methods on our user object so they can access Facebook information we can start by creating a new file app/models/user/facebook_methods.rb (you’ll need to create the user folder).

This file is where we’ll group all of our methods related to Facebook. For this example i’ll be using the Koala Facebook gem, and we assume our user model has a facebook_token attribute persisted to the database.

module User::FacebookMethods
  extend ActiveSupport::Concern

  def facebook_graph
    @facebook_graph ||= Koala::Facebook::API.new(facebook_token)

Not a bad start, now we want to add this ability to our user model, open up app/models/user.rb and add our concern.

class User < ActiveRecord::Base
  include User::FacebookMethods

Great! Now we can construct our Facebook graph object straight from our user.

user = User.where("facebook_token is not null").first
# => <# Koala #...

user.facebook_graph.get_connections("me", "friends")
# => {52930 => 'Terence Lee',  12345 => "Ruby Ku" #...

That Was Easy, but…

What did that buy us? First we’ve got an obvious place to store our code. Want to write a method that pulls out all of a user’s Facebook friends? Put it in the facebook_methods.rb file. If you forget the name of that method, check out your Facebook methods. If you need the facebook_graph method, bet you money it’s in that concern. If all related code is in one place its a lot easier to scan visually and to search for keywords.

Won’t This add More Code to the Codebase?

In the example above, we added 4 extra lines of code to save us 3 measly lines in our user.rb file. While this isn’t ideal for such a small concern, as it grows in size its much easier to keep track of the components, which in turn helps keep your code small and manageable. It also nudges developers to create unit tests for those specific concerns. This sounds minor, but when you get to a file with 3500 lines of code, you start duplicating functionality that you didn’t know existed. Either it was added by another developer, or you forgot you added it months ago. Keeping everything in its place helps keep your code sane.

How Should I Break up My Code

Often times I like breaking out my concerns based on knowledge of third party services. For example I broke out a concern for all the Facebook methods above. I use websolr and I like having a separate concern for all the search related methods. Recently I played around with the Ice Cube gem which is a library for creating an iCal formatted recurring date syntax. I split that code out into a concern, not because it was touching another service, but because I might want to re-use that code in another model some day, and it’s easier to break out the code now. There are no hard and fast rules, just don’t go overboard and have 100 concerns for every model with 3 lines of code in each of them, and on the flip side don’t have one concern with everything.

Just think of the different areas of ‘concern’, that your code covers. Get it?

Legacy Code

While building out the final version of the Gowalla service we managed to promote Redis and Cassandra to first class data storage citizens, completely re-write all web controllers, and split out a brand new api into a separate set of controllers (more on this in a later date). It was a ton of work, we were completely changing the way our service worked and creating new paradigms such as a “Stories” where multiple users could check each other in and at the same time, we still had to support a ton of 3rd party client applications using the old API.

So, how did concerns help? We used concerns to isolate new code and new services. It also helped us to add more & better unit testing by focusing different spec files on different areas so models/users/following_methods.rb would be tested by spec/models/users/following_methods_spec.rb. Most of the developers used some form of automatic test runner such as Guard Rspec, and it is nice being able to run only the unit tests associated with the concern you’re writing without having to run all the unit tests for that model.

Bonus! Ever find a method that you were pretty sure wasn’t being called by anything. Maybe it’s in a model that wasn’t exactly 100% tested. You could try making a concern for methods of questionable value. In a month if it’s still there, delete the sucker, deploy to staging, validate and commit to master.

Shared Concerns

If you have code that needs to be shared by multiple models in your project, you can keep this in your lib folder. I actually like to have a concerns folder like lib/concerns/models/duplicate_code.rb. When we added Cassandra to the Gowalla project we needed a way to get our Postgres models to play nice. Thats when Adam Keys and Bill Doughty pulled out common logic and put it into a concern using another library called Chronologic. lib/concerns/models/chronologify.rb

Then any time you wanted this shared code into your model, you just had to include it.

# models/checkin.rb
class Checkin < ActiveRecord::Base
  include Concerns::Models::Chronologify

Don’t forget to add the concern folder in your lib to your search path.

Wrap it Up

There have been tomes written about dealing with legacy code in software, I’ve been recently recommended Working Effectively with Legacy Code . Using concerns won’t be a magic bullet, but it will help keep your code nicely organized. Even if you’re dealing with a pristine new app, concerns are one way to help it stay that way. Give concerns a try and let me know if you have a good or bad experience @schneems.