Ruby On Rails Best Practices #2

Techie     July 2022

Introduction

Good programming practices promote readability and maintainability of the code and also reduces complexity. These coding standards enables a team to have a better overall understanding of a project, which leads to fewer errors and a reduced need for constant fixes.

It is good to learn the best practices early on to make the most out of any programming language. This section describes some of the best practices for the RoR framework.


1. Rescue from StandardError Rather Than Exception

Rescuing from Exception broadens the scope rather than narrowing it, and can have catastrophic results and make bug-hunting a pain. Exception is the root of Ruby’s exception hierarchy, so when you rescue Exception you rescue from everything, including subclasses such as SyntaxError, LoadError, and Interrupt.

By default Ruby will rescue from StandardError

begin
  # ...
rescue
  # ...
end


If you need a variable with the exception, you could do this:

begin
  # ...
rescue => e
  # ...
end


# That's also equivalent to:

begin
  # ...
rescue StandardError => e
  # ...
end


However, it is okay to rescue from Exception for logging/reporting purposes, in which case you should immediately re-raise the exception:

begin
  # ...
rescue Exception => e
  # do some logging
  raise # ...
end


2. Don’t Violate The Law Of Demeter

In programming the Law Of Demeter is not really a law but a guidline that states: an entity or object should not call methods through another entity or object.

For instance, say you have 3 related models: Author, Article and Comment

class Author < ActiveRecord::Base
  has_many :books
end

class Book < ActiveRecord::Base
  belongs_to :author
  has_many :reviews  
end

class Review < ActiveRecord::Base
  belongs_to :book
end


Suppose you want to get information about the author related to a given review? Doing the following would work:

<%= @review.book.author.name %>

<%= @review.book.author.country %>


But using several dots is a violation of of the Law of Demeter which would have you use only one. To avoid the multi dot syndrome, Rails provides a delegate method which helps you shorten your methods. To achieve that, you need to delegate Author methods in Book model:

class Book < ActiveRecord::Base
  belongs_to :author
  has_many :reviews 
  
  delegate :name, :country, to: :author, allow_nil: true
     
end
  


Now Book can use Author methods. But since you’re using the methods from Review instance, you also need to delegate the methods in Review model:


class Review < ActiveRecord::Base
  belongs_to :book

  delegate :name, :country, to: :book, prefix: 'book_author', allow_nil: true    
end


If Book or Author object is nil, you will have NoMethodError raised. You avoid that by specifying allow_nil: true in delegate method.


Now you can easily use those methods from the Review instance:

<%= @review.book_author_name %>

<%= @review.book_author_country %>


NB: You could also use prefix: true which gets class name provided to :to hash and uses it as a prefix. In this case that will result in:

<%= @review.book_name %>

<%= @review.book_country %>

Not really good for this example hence why prefix: ‘book_author’ is preferable.


In other cases, you would be tempted to access information across models by chaining together methods such as @author.book.all

Again, 2 dots are too many.

You would need to create a helper method in the Author class that would achieve this for you.

class Author < ActiveRecord::Base
  has_many :books
  
  def all_books
    self.books.all
  end  
end


Now you would be able to adhere to the design priciple by only having to use one dot to access the same information:

<%= @author.all_books %>



3. Use ? At the End of Method Name If It Is Returning Boolean Value

def all_orders_processed?
  #...
end



4. Declare Instance Variables Inside the Action

Instance variables should not be placed in private methods but rather declared inside the action. This increases readability and eliminates confusion.

before_filter :get_book

  def show
    @book = get_book
    @reviews = @book.reviews
  end
  
private
  def get_book
    Book.find(params[:id])
  end


###### Don't do this:

before_filter :get_book

  def show
    @reviews = @book.reviews
  end
  
private
  def get_book
    @book = Book.find(params[:id])
  end  
  


5. Make Helper Methods for Views

Avoid filling the views with calculations but when it’s unavoidable, do the processing with helpers.

Don’t do this:

<%= f.select( :genre, ['Literary Fiction', 'Mystery', 'Thriller', 'Horror', 'Historical', 'Romance', 'Western'].collect{|genre| [genre, genre]}) %>


Do this instead:

<%= f.select( :genre, genre_names.collect{|genre|  [genre, genre]}) %>


And create a helper:

  def  genre_names
    ['Literary Fiction', 'Mystery', 'Thriller', 'Horror', 'Historical', 'Romance', 'Western']
  end


The same technique can be applied for conditional displays:

Don’t do this:

<% case @filter %>
<% when 'inbox' %>
    <%= render 'inbox'%>
<% when 'sent' %>
    <%= render 'sent' %>
<% when 'draft' %>
    <%= render 'draft' %>
<% when 'trash'%>
    <%= render 'trash' %>
<% end %>


Do this instead:

<%= render filter_templates(@filter) %>


And create a helper:

  def filter_templates(filter)
    case filter
    when 'inbox'
        render 'inbox'
    when 'sent'
        render 'sent'
    when 'draft'
        render 'draft'
    when 'trash'
        render  'trash'
    end
  end


6. Keep it simple, stupid (KISS)

Keep it simple, stupid (KISS) is a design principle which states that designs and/or systems should be as simple as possible. In modern era the principle has evolved to have less derogatory implications and if you so wish you may also refer to is as: Keep It Short and Simple or Keep It Short and Sweet.

The easier something is to understand and use, the more likely it is to be adopted and engaged with. Simplicity should be a primary design goal to reduce complexity. It can be achieved by following the “Single Responsibility Principle” among other techniques.


7. Continuous Refactoring

Continuous improvement of a programs internal structure ensures that the product is being done according to best practice and does not degenerate to a patch work. The process should be a constant and gradual improvement to your codebase.


8. Prevent SQL Injection

Do not supply user input as a database query without escaping quotes. If the user passes a single quote in an input text, then the text after the single quote character is considered to be an SQL statement. This means that the text would have direct access to the database, putting the entire database at risk as the user might have entered malicious content.

# Dont do this
  Book.where("name = '#{params[:name]}'")
  
  Author.find_by("id = '#{params[:author_id]'")


# Instead, pass it as a parameter: 
  Book.where("name like ?", params[:name])
  
  Author.find_by(id: params[:author_id])
  


9. Write Tests

Testing is an absolute best practice in software development. Running your Rails tests you can ensure your code adheres to the desired functionality even after some major code refactoring.

Rails tests can also simulate browser requests and thus you can test your application’s response without having to test it through your browser. Tests also acts as detailed specifications of a feature or application and as documentation for other engineers, which helps them understand your intent in an implementation.


10. Make Use of Enums

An enum is an attribute where the values map to integers in the database and can be queried by name.

Say you wanted to store the status attribute of the book object as either completed, draft or published. You may do something like this:

  if book.status == "draft"
    # ...
  elsif book.status == "completed"
    # ...
  elsif book.status == "published"
    # ...
  end

# or you could do

  if book.status == 0 #draft
      # ...
  elsif book.status == 1 #completed
      # ...
  elsif book.status == 2 #published
      # ...
  end

Which is not very elegant.

A better approach would be to define an enum for the status attribute of Book object, where the possible values are draft, completed, or published. After refactoring the code, it would look like this:

  enum status: { draft: 0, completed: 1, published: 2 }

  if book.draft?
      # ...
  elsif book.completed?
      # ...
  elsif book.published?
      # ...
  end
  

Ruby on Rails also provides a helper for updating the enum value. Instead of Book.update(status: :published), we can use: Book.published!

You can also generate a scope like so: Book.draft instead of doing Book.where(status: ‘draft’)

Lastly, you could make the scope more intuitive by adding the _prefix: true or _suffix: true option:

  enum status: { draft: 0, completed: 1, published: 2 }, _prefix: true

  Book.status_completed?  # status == 'completed'
  
  Book.status_published!  # update(status: :published)
  
  Book.status_draft  # Book.where(status: :draft)
  


Thanks for reading, see you in the next one!