Dev

13 tips to write better Rails code

Profil Picture

Guillaume Briday

7 minutes

Writing clean code is the key for maintainable and sustainable code. Here's some of my favorite tips I could give you after many years working on Rails and Ruby.

1. Install rubocop

Definitely, one of the most popular Gem on the Web. Rubocop is a static code analyzer and formatter.

If you write Ruby code, you must use it! You don't have to fix every single warning or error but at least you must know that they are here. You could also write your own rules or disable those you don't want.

You will learn so much things and good practices with Rubocop, it's a must have.

You should check its extensions too:

It's also very useful to remove deprecated methods and bad practises. It helps you to upgrade your application too.

2. Using hash instead of mutiple params

It's probably a code smell to define too many parameters in your method signature.

Even if parameters have default value or are optional.

Why? For several reasons, let's take this example:

def picture_url(picture, format = :small, full_url = false, caption = true)
  # Doing stuff
end
<%= picture_url(@user.avatar, :thumb, true, false) %>

When you use this method it's very hard to know what parameters are given to the method.

What the last two params, true and false, stand for here ? I need to open the method definition to know.

And moreover, if you need to change only one param, you have to redefined all others.

With a hash, it's easier:

def picture_url(picture, format: :small, full_url: false, caption: true)
<%= picture_url(@user.avatar, format: :thumb, caption: false) %>

In summary:

# Don't
def picture_url(picture, format = :small, full_url = false, caption = true)

# Do
def picture_url(picture, format: :small, full_url: false, caption: true)

3. Using scope for explicitness, not only DRY

Scopes are generally used or presented to DRY up your code.

You should write scope for commonly-used queries in your code.

I think it's partially true.

Scopes are very useful for readability and to describe what we really want to do. It helps other developers to know what's going on in the query if they have lots of keywords.

For instance:

# Don't
def index
  @posts = Post.where(published: true).where("published_at > ?", Time.current).order(:published_at)
end

# Do
def index
  @posts = Post.published.order(:published_at)
end

class Post < ApplicationRecord
  scope :published, -> { where(published: true).where("published_at > ?", Time.current) }
end

This scope might be used only once, but it improves the readability of the query in the controller and could be use in other scopes as well.

Another example, without the scope it could be hard to know why we would check the title's length:

scope :with_long_title, ->(length = 20) { where("LENGTH(title) > ?", length) }

4. Extract your code

There are lots of patterns to help you to extract your code. Using scope is one of them.

Sometimes we should extract more than we actually do.

You can use helpers, partials and layouts, gems, Decorators, Services, Form Object, Presenters and so on.

It will increase the readability and DRY up your code. It's also simpler to edit code in a dedicated class or helper.

Here, you don't have to understand what the params for options_for_select do, because it's self-documented in the helper's name.

# Don't
= select_tag :state, options_for_select(User.states.keys.map { |state| [User.human_attribute_name("states.#{state}"), state] })

# Do
def user_states_as_options
  User.states.keys.map do |state|
    [User.human_attribute_name("states.#{state}"), state]
  end
end

= select_tag :state, options_for_select(user_states_as_options)

Extract in methods or variables when conditions are too complex.

# Don't
if (user.present? && user.member?) && Date.current > post.published_at || post.comments.count > 0 && post.comments_opened?
  # Some stuff
end

# Do
def user_is_member?
  user.present? && user.member?
end

def post_published?
  Date.current > post.published_at
end

def post_has_comments?
  post.comments.count > 0 && post.comments_opened?
end

if user_is_member? && post_published? || post_has_comments?
  # Some stuff
end

5. Return early

Nested conditions increase the cognitive load you need to understand the code, avoid it with guard clauses and by returning earlier.

It will remove most of your else conditions too. You will read your code like English.

# Don't
def some_method
  if user.present?
    if user.member?
      'Some'
    else
      'Stuff'
    end
  else
    'Here'
  end
end

# Do
def some_method
  return 'Here' if user.blank?
  return 'Some' if user.member?

  'Stuff'
end

6. Learn and use the power of your tools

Ruby and Rails provide tons of methods on Array, Hash, String and basically everything single Object.

Find and learn them to enjoy the full power of the ecosystem.

# Don't
def some_methods
  users_name = []

  User.all.each do |user|
    users_name << user.name
  end

  users_name
end

# Do
def some_method
  User.all.collect { |user| user.name }
  User.all.collect(&:name) # Shorthand
end
# Don't
items.select { |item| item.name == other_item.name }.first

# Do
items.find { |item| item.name == other_item.name }

7. Code for humans, not for computers

Humans are bad at comparing things and being sure that two lines of code are almost the same.

These two lines differ by only one scope. If you need to change something for one, then you also need to change it for the other.

# Don't
def index
  @admins = User.admin.order(:position).enabled.limit(10).includes(:avatar)
  @members = User.order(:position).enabled.limit(10).includes(:avatar).members
end

# Do
def index
  users = User.order(:position).enabled.limit(10).includes(:avatar)

  @admins = users.admin
  @members = users.members
end

Self-documented variable assignation. This condition is only used to define the variable value and Ruby makes it readable like English.

# Don't
if country == :usa
  unit = '$'
elsif country == :fr
  unit = '€'
end

# Do
unit = case country
       when :usa then '$'
       when :fr then '€'
       end

Do not use magic numbers. Use variables, constants or methods instead:

# Don't
def price_with_taxes(price)
  price * (1.0 + 0.2)
end

# Do
TAX_RATE = 0.2

def price_with_taxes(price)
  price * (1.0 + TAX_RATE)
end

8. Define multiple has_many on the same associations

You can define different has_many associations on the same foreign_key.

# Don't
class Team < ApplicationRecord
  has_many :members

  def managers
    members.where(manager: true)
  end
end

# Do
class Team < ApplicationRecord
  has_many :members
  has_many :managers, -> { where(manager: true) }, class_name: 'Member'
end

It feels more natural to describe your associations with Active Record Associations instead of instance methods.

9. Don't use abbreviations

Abbreviations don't make you write your code faster!

I mean, write user or usr, ctx or context is the same. And don't forget that you have a muscle memory when you're writing common words.

# Don't
def some_method
  accommodation_buildings.each do |ab|
    ab.apartments.each do |ap|
      "#{ap.name} in #{ab.name}"
    end
  end
end

# Do
def some_method
  accommodation_buildings.each do |accommodation_building|
    accommodation_building.apartments.each do |apartment|
      "#{apartment.name} in #{accommodation_building.name}"
    end
  end
end

You don't have to remember what ap and ab stand for, you just read it.

Business rules are enough complex to not bloat your mind with abbreviations.

This applies to variables, class, methods and basically everything.

There are few exceptions though. It's ok to use widely used abbreviations like i in for-loop or id instead of identifier.

10. Avoid useless conditions

Conditions are like temporary variables for your brain. They have to be in your memory for a period of time in order to know what code is going to be executed.

Fortunately, most of them can be avoided.

# Don't
@post = Post.find(params[:id]) if params[:id].present?

# Do
@post = Post.find_by(id: params[:id])
# Don't
def some_method(param)
  return 'Foo' if param.present? && param == :something

  'Bar'
end

# Do
def some_method(param)
  return 'Foo' if param == :something

  'Bar'
end

Using delegate:

# Don't
- if post.category.present?
  = post.category.name

# Do
class Post < ApplicationRecord
  belongs_to :category, optional: true
  delegate :name, to: :category, prefix: true, allow_nil: true
end

= post.category_name
# Don't
if users.any?
  users.each do |user|
    # Some stuff
  end
end

# Do
users.each do |user|
  # Some stuff
end
# Don't
if user.member?
  users.where(member: true)
else
  users.where(member: false)
end

# Do
users.where(member: user.member?)

11. Negated conditions and ternaries

Computing conditions in our brain is not an easy task. Don't make it harder with double negation.

It feels more natural to read Is this user present and a member? than Is this user is not absent and not an admin?, right?

Sometimes, you will have to completely rewrite your code to understand what exactly is happening.

# Don't
def some_method
  unless user.blank? || user.not_admin?
    # Some stuff
  else
    # Some other stuff
  end
end

# Do
def some_method
  if user.present? && user.member?
    # Some stuff
  else
    # Some other stuff
  end
end

I generally never use the keyboard unless nor I write methods with negation or at least I write the positive alternative:

# Don't
def not_admin?
  # my condition
end

# Do
def not_admin?
  # my condition
end

def member?
  !not_admin?
end

Same for ternaries, it could be even worst with nested one's:

# Don't
def some_method
  unless user.blank? ? (user.not_admin? ? 'Some' : 'Stuff') : 'Here'
end

# Do
def some_method
  return 'Here' if user.blank?
  return 'Some' if user.member?

  'Stuff'
end

Sometimes you have to write ternaries, in inline blocks for example, then try to use positive conditions:

# Don't
users.collect { |user| user.not_admin? ? 'Member' : 'Admin' }

# Do
users.collect { |user| user.member? ? 'Member' : 'Admin' }

Try to use the positive version of methods available to prevent double negations:

# Don't
users.reject { |user| user.not_admin? }
users.reject(&:not_admin?) # shorthand version

# Do
users.select { |user| user.member? }
users.select(&:member?) # shorthand version

12. Extract in gem

Creating a gem is not that complicated. For instance, I need to create a lot of link_to with

target: '_blank', rel: 'noopener noreferrer' attributes on a daily basis.

Instead of writing these attributes multiple times a day, I created the gem external link to with these attributes by default around the default link_to helper:

# Don't
= link_to 'Home', root_path, rel: 'noopener noreferrer', target: '_blank'

# Do
= external_link_to 'Home', root_path

13. Readability is better than cleverness

Remember, you're writing code with other developers (or/and your future yourself) to solve users' problems.

Don't write code to show off your skills or your intelligence.

Our brain cycles are more valuable than our CPU cycles. Let the computer do the hard work.

You don't need to find a way to reduce your 15 lines of clear code into 3 that no one would ever understand.

Stupid code is easier to debug!

Keep it simple, stupid.

Conclusion

I hope this helps.

Thanks. 👋

Simplify your time tracking with Timecop

Timecop is a time tracking app that brings simplicity in your day to day life.

Timecop projects