ruby

8 Essential Ruby on Rails Best Practices for Clean and Efficient Code

Discover 8 best practices for clean, efficient Ruby on Rails code. Learn to optimize performance, write maintainable code, and leverage Rails conventions. Improve your Rails skills today!

8 Essential Ruby on Rails Best Practices for Clean and Efficient Code

Ruby on Rails has been a go-to framework for web development since its inception. Its convention over configuration philosophy and elegant syntax have made it a favorite among developers. However, as projects grow in complexity, maintaining clean and efficient code becomes crucial. In this article, I’ll share eight best practices that have proven invaluable in my experience with Ruby on Rails.

  1. Embrace the DRY Principle

Don’t Repeat Yourself (DRY) is a fundamental principle in software development, and it’s particularly emphasized in Rails. The idea is simple: every piece of knowledge or logic should have a single, unambiguous representation within a system. In Rails, we have several tools at our disposal to achieve this.

One of the most powerful ways to implement DRY in Rails is through the use of concerns. Concerns allow us to extract common functionality into modules that can be shared across multiple models or controllers. Here’s an example:

module Searchable
  extend ActiveSupport::Concern

  included do
    scope :search, ->(query) { where("name LIKE ?", "%#{query}%") }
  end

  def highlight_matches(query)
    name.gsub(/(#{query})/i, '<strong>\1</strong>')
  end
end

class Product < ApplicationRecord
  include Searchable
end

class Category < ApplicationRecord
  include Searchable
end

In this example, we’ve created a Searchable concern that provides a search scope and a highlight_matches method. By including this concern in both Product and Category models, we avoid duplicating this functionality.

  1. Follow SOLID Principles

SOLID is an acronym for five design principles that help make software designs more understandable, flexible, and maintainable. While these principles were originally conceived for object-oriented programming, they apply well to Rails development.

Let’s focus on the Single Responsibility Principle (SRP) as an example. This principle states that a class should have only one reason to change. In Rails, we often see this violated in models that become bloated with various responsibilities.

Consider a User model that handles authentication, profile management, and order processing:

class User < ApplicationRecord
  has_secure_password
  has_many :orders

  def update_profile(params)
    # Update user profile
  end

  def place_order(product)
    # Place an order
  end

  def calculate_total_spent
    # Calculate total spent on orders
  end
end

This User model is doing too much. We can improve it by extracting responsibilities into separate classes:

class User < ApplicationRecord
  has_secure_password
  has_many :orders
end

class ProfileManager
  def initialize(user)
    @user = user
  end

  def update(params)
    # Update user profile
  end
end

class OrderProcessor
  def initialize(user)
    @user = user
  end

  def place_order(product)
    # Place an order
  end
end

class SpendingCalculator
  def initialize(user)
    @user = user
  end

  def total_spent
    # Calculate total spent on orders
  end
end

By breaking down the responsibilities into separate classes, we’ve made our code more modular and easier to maintain.

  1. Keep Controllers Skinny

Controllers in Rails should act as a thin layer between the HTTP request and your application’s business logic. They should primarily be responsible for handling request parameters, invoking the appropriate business logic, and preparing data for the view.

A common anti-pattern is to stuff business logic directly into controllers. Instead, we should aim to move this logic into models or service objects. Here’s an example of a controller that’s doing too much:

class OrdersController < ApplicationController
  def create
    @order = Order.new(order_params)
    if @order.save
      if @order.total > 100
        ApplyDiscount.new(@order).call
      end
      OrderMailer.confirmation(@order).deliver_now
      redirect_to @order, notice: 'Order was successfully created.'
    else
      render :new
    end
  end

  private

  def order_params
    params.require(:order).permit(:user_id, :total)
  end
end

We can improve this by moving the business logic into a service object:

class OrdersController < ApplicationController
  def create
    result = CreateOrder.new(order_params).call
    if result.success?
      redirect_to result.order, notice: 'Order was successfully created.'
    else
      @order = result.order
      render :new
    end
  end

  private

  def order_params
    params.require(:order).permit(:user_id, :total)
  end
end

class CreateOrder
  def initialize(params)
    @params = params
  end

  def call
    order = Order.new(@params)
    if order.save
      apply_discount(order) if order.total > 100
      send_confirmation_email(order)
      OpenStruct.new(success?: true, order: order)
    else
      OpenStruct.new(success?: false, order: order)
    end
  end

  private

  def apply_discount(order)
    ApplyDiscount.new(order).call
  end

  def send_confirmation_email(order)
    OrderMailer.confirmation(order).deliver_now
  end
end

This approach keeps our controller skinny and moves the business logic into a dedicated service object.

  1. Leverage Rails Conventions

Rails is opinionated, and that’s a good thing. By following Rails conventions, we write code that’s more consistent and easier for other Rails developers to understand. Here are a few key conventions to keep in mind:

  • Use plural names for controllers (e.g., ProductsController)
  • Use singular names for models (e.g., Product)
  • Follow the standard RESTful actions in controllers (index, show, new, create, edit, update, destroy)
  • Use snake_case for file names and method names
  • Use CamelCase for class and module names

Here’s an example of a controller following these conventions:

class ProductsController < ApplicationController
  def index
    @products = Product.all
  end

  def show
    @product = Product.find(params[:id])
  end

  def new
    @product = Product.new
  end

  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to @product, notice: 'Product was successfully created.'
    else
      render :new
    end
  end

  private

  def product_params
    params.require(:product).permit(:name, :price)
  end
end
  1. Use Service Objects for Complex Business Logic

As our application grows, we often encounter complex business logic that doesn’t fit neatly into models or controllers. This is where service objects come in handy. Service objects encapsulate a single piece of business logic, making our code more modular and easier to test.

Here’s an example of a service object that handles the process of placing an order:

class PlaceOrder
  def initialize(user, product)
    @user = user
    @product = product
  end

  def call
    ActiveRecord::Base.transaction do
      create_order
      update_inventory
      send_confirmation_email
    end
    @order
  end

  private

  def create_order
    @order = @user.orders.create!(product: @product, total: @product.price)
  end

  def update_inventory
    @product.decrement!(:stock_count)
  end

  def send_confirmation_email
    OrderMailer.confirmation(@order).deliver_later
  end
end

We can then use this service object in our controller:

class OrdersController < ApplicationController
  def create
    @order = PlaceOrder.new(current_user, Product.find(params[:product_id])).call
    redirect_to @order, notice: 'Order was successfully placed.'
  rescue ActiveRecord::RecordInvalid
    redirect_to products_path, alert: 'Unable to place order.'
  end
end

This approach keeps our controller clean and makes our business logic easy to understand and test.

  1. Write Comprehensive Tests

Testing is crucial for maintaining a healthy Rails application. A comprehensive test suite gives us confidence when refactoring or adding new features. In Rails, we have several types of tests at our disposal:

  • Unit tests for models and other Ruby classes
  • Controller tests for testing HTTP responses
  • Integration tests for testing user flows
  • System tests for testing the application from the user’s perspective

Here’s an example of a model test using RSpec:

RSpec.describe Product, type: :model do
  it "is valid with valid attributes" do
    product = Product.new(name: "Test Product", price: 9.99)
    expect(product).to be_valid
  end

  it "is not valid without a name" do
    product = Product.new(price: 9.99)
    expect(product).to_not be_valid
  end

  it "is not valid without a price" do
    product = Product.new(name: "Test Product")
    expect(product).to_not be_valid
  end

  it "is not valid with a negative price" do
    product = Product.new(name: "Test Product", price: -9.99)
    expect(product).to_not be_valid
  end
end

And here’s an example of a controller test:

RSpec.describe ProductsController, type: :controller do
  describe "GET #index" do
    it "returns a success response" do
      get :index
      expect(response).to be_successful
    end

    it "assigns all products as @products" do
      product = Product.create!(name: "Test Product", price: 9.99)
      get :index
      expect(assigns(:products)).to eq([product])
    end
  end

  describe "POST #create" do
    context "with valid params" do
      it "creates a new Product" do
        expect {
          post :create, params: {product: {name: "New Product", price: 19.99}}
        }.to change(Product, :count).by(1)
      end

      it "redirects to the created product" do
        post :create, params: {product: {name: "New Product", price: 19.99}}
        expect(response).to redirect_to(Product.last)
      end
    end

    context "with invalid params" do
      it "returns a success response (i.e. to display the 'new' template)" do
        post :create, params: {product: {name: "Invalid Product"}}
        expect(response).to be_successful
      end
    end
  end
end
  1. Use Scopes and Class Methods Effectively

ActiveRecord provides powerful tools for querying the database. Two of these tools are scopes and class methods. While they can often be used interchangeably, understanding their differences can help us write more expressive and efficient queries.

Scopes are a way to define commonly-used queries as method calls on the model. They always return an ActiveRecord::Relation, which allows them to be chained with other scopes or methods.

class Product < ApplicationRecord
  scope :in_stock, -> { where("stock_count > 0") }
  scope :price_above, ->(price) { where("price > ?", price) }
end

# Usage
Product.in_stock.price_above(100)

Class methods, on the other hand, can be more flexible. They can return anything, not just an ActiveRecord::Relation. They’re useful when you need to do something more complex than a simple query.

class Product < ApplicationRecord
  def self.top_sellers(limit = 5)
    select("products.*, SUM(order_items.quantity) as total_sold")
      .joins(:order_items)
      .group("products.id")
      .order("total_sold DESC")
      .limit(limit)
  end
end

# Usage
Product.top_sellers(10)

In general, use scopes for simple queries that return a relation, and use class methods for more complex operations or when you need to return something other than a relation.

  1. Optimize Database Queries

As our application grows, database performance often becomes a bottleneck. Rails provides several tools to help us optimize our database queries. Here are a few techniques:

  • Use includes to avoid N+1 queries:
# Bad (N+1 query)
@orders = Order.all
@orders.each do |order|
  puts order.user.name
end

# Good
@orders = Order.includes(:user).all
@orders.each do |order|
  puts order.user.name
end
  • Use pluck when you only need specific columns:
# Instead of
User.all.map(&:email)

# Use
User.pluck(:email)
  • Use find_each for batching when dealing with large datasets:
User.find_each do |user|
  NewsMailer.weekly(user).deliver_now
end
  • Use counter_cache to avoid counting associated records:
class Product < ApplicationRecord
  has_many :reviews, counter_cache: true
end

class Review < ApplicationRecord
  belongs_to :product, counter_cache: true
end

# Now you can use product.reviews_count instead of product.reviews.count

These practices have served me well in my journey with Ruby on Rails. They’ve helped me write cleaner, more maintainable code, and build more robust applications. Remember, these are guidelines, not strict rules. Always consider your specific context and requirements when applying these practices.

Writing clean and maintainable code is an ongoing process. It requires constant learning, refactoring, and improvement. But by following these best practices, we can create Rails applications that are a joy to work with and easy to maintain over time.

As you continue your Rails journey, keep exploring new techniques and best practices. The Rails community is constantly evolving and coming up with new ways to write better code. Stay curious, keep learning, and happy coding!

Keywords: ruby on rails best practices, rails code optimization, DRY principle in rails, SOLID principles for rails, rails controller best practices, service objects in rails, rails testing strategies, activerecord query optimization, rails performance tips, rails convention over configuration, rails model design, skinny controllers fat models, rails refactoring techniques, rails code organization, rails database optimization, n+1 query problem rails, rails caching strategies, rails app scalability, rails design patterns, ruby metaprogramming in rails



Similar Posts
Blog Image
How Can Sentry Be the Superhero Your Ruby App Needs?

Error Tracking Like a Pro: Elevate Your Ruby App with Sentry

Blog Image
7 Advanced Techniques for Building Scalable Rails Applications

Discover 7 advanced techniques for building scalable Rails applications. Learn to leverage engines, concerns, service objects, and more for modular, extensible code. Improve your Rails skills now!

Blog Image
Mastering Rust's Existential Types: Boost Performance and Flexibility in Your Code

Rust's existential types, primarily using `impl Trait`, offer flexible and efficient abstractions. They allow working with types implementing specific traits without naming concrete types. This feature shines in return positions, enabling the return of complex types without specifying them. Existential types are powerful for creating higher-kinded types, type-level computations, and zero-cost abstractions, enhancing API design and async code performance.

Blog Image
Boost Your Rails App: Implement Full-Text Search with PostgreSQL and pg_search Gem

Full-text search with Rails and PostgreSQL using pg_search enhances user experience. It enables quick, precise searches across multiple models, with customizable ranking, highlighting, and suggestions. Performance optimization and analytics further improve functionality.

Blog Image
How Can Ruby Transform Your File Handling Skills into Wizardry?

Unleashing the Magic of Ruby for Effortless File and Directory Management

Blog Image
Optimize Rails Database Queries: 8 Proven Strategies for ORM Efficiency

Boost Rails app performance: 8 strategies to optimize database queries and ORM efficiency. Learn eager loading, indexing, caching, and more. Improve your app's speed and scalability today.