Ruby on Rails


Rails Best Practices All Versions

1.0
1.1
1.2
2.0
2.3
3.0
3.1
3.2
4.0
4.1
4.2
5.0

This draft deletes the entire topic.

expand all collapse all

Examples

  • 10

    “Fat Model, Skinny Controller” refers to how the M and C parts of MVC ideally work together. Namely, any non-response-related logic should go in the model, ideally in a nice, testable method. Meanwhile, the “skinny” controller is simply a nice interface between the view and model.

    In practice, this can require a range of different types of refactoring, but it all comes down to one idea: by moving any logic that isn’t about the response to the model (instead of the controller), not only have you promoted reuse where possible but you’ve also made it possible to test your code outside of the context of a request.

    Let’s look at a simple example. Say you have code like this:

    def index
      @published_posts = Post.where('published_at <= ?', Time.now)
      @unpublished_posts = Post.where('published_at IS NULL OR published_at > ?', Time.now)
    end
    

    You can change it to this:

    def index
      @published_posts = Post.published
      @unpublished_posts = Post.unpublished
    end
    

    Then, you can move the logic to your post model, where it might look like this:

    scope :published, ->(timestamp = Time.now) { where('published_at <= ?', timestamp) }
    scope :unpublished, ->(timestamp = Time.now) { where('published_at IS NULL OR published_at > ?', timestamp) }
    
  • 9

    In Rails you find yourself looking at controllers, views and models for your database.

    In order to reduce the need of heavy configuration, Rails implements rules to ease up working with the application. You may define your own rules but for the beginning (and for later on) it's a good idea to stick to conventions that Rails offers.

    These conventions will speed up development, keep your code concise and readable and allow you an easy navigation inside your application.

    Conventions also lower the barriers of entry for beginners. There are so many conventions in Rails that a beginner doesn’t even need to know about, but can just benefit from in ignorance. It’s possible to create great applications without knowing why everything is the way it is.

    For Example

    If you have a database table called orders with the primary key id. The matching model is called order and the controller that handles all the logic is named orders_controller. The view is split in different actions: if the controller has a new and edit action, there is also a new and edit view.

    For Example

    To create an app you simply run rails new app_name. This will generate roughly 70 files and folders that comprise the infrastructure and foundation for your Rails app.

    It includes:

    • Folders to hold your models (database layer), controllers, and views
    • Folders to hold unit tests for your application
    • Folders to hold your web assets like Javascript and CSS files
    • Default files for HTTP 400 responses (i.e. file not found)
    • Many others
  • 6

    ActiveRecord includes default_scope, to automatically scope a model by default.

    class Post
      default_scope ->{ where(published: true).order(created_at: :desc) }
    end
    

    The above code will serve posts which are already published when you perform any query on the model.

    Post.all # will only list published posts 
    

    That scope, while innocuous-looking, has multiple hidden side-effect that you may not want.

    default_scope and order

    Since you declared an order in the default_scope, calling order on Post will be added as additional orders instead of overriding the default.

    Post.order(updated_at: :desc)
    
    SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."created_at" DESC, "posts"."updated_at" DESC
    

    This is probably not the behavior you wanted; you can override this by excluding the order from the scope first

    Post.except(:order).order(updated_at: :desc)
    
    SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."updated_at" DESC
    

    default_scope and model initialization

    As with any other ActiveRecord::Relation, default_scope will alter the default state of models initialized from it.

    In the above example, Post has where(published: true) set by default, and so new models from Post will also have it set.

    Post.new # => <Post published: true>
    

    unscoped

    default_scope can nominally be cleared by calling unscoped first, but this also has side-effects. Take, for example, an STI model:

    class Post < Document
      default_scope ->{ where(published: true).order(created_at: :desc) }
    end
    

    By default, queries against Post will be scoped to type columns containing 'Post'. But unscoped will clear this along with your own default_scope, so if you use unscoped you have to remember to account for this as well.

    Post.unscoped.where(type: 'Post').order(updated_at: :desc)
    

    unscoped and Model Associations

    Consider a relationship between Post and User

    class Post < ApplicationRecord
      belongs_to :user
      default_scope ->{ where(published: true).order(created_at: :desc) }
    end
    
    class User < ApplicationRecord
      has_many :posts
    end
    

    By getting an individual User, you can see the posts related to it:

    user = User.find(1)
    user.posts
    
    SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' AND "posts"."user_id" = ? ORDER BY "posts"."created_at" DESC [["user_id", 1]]
    

    But you want to clear the default_scope from the posts relation, so you use unscoped

    user.posts.unscoped
    
    SELECT "posts".* FROM "posts"
    

    This wipes out the user_id condition as well as the default_scope.

    An example use-case for default_scope

    Despite all of that, there are situations where using default_scope is justifiable.

    Consider a multi-tenant system where multiple subdomains are served from the same application but with isolated data. One way to achieve this isolation is through default_scope. The downsides in other cases become upsides here.

    class ApplicationRecord < ActiveRecord::Base
      def self.inherited(subclass)
        super
    
        return unless subclass.superclass == self
        return unless subclass.column_names.include? 'tenant_id'
    
        subclass.class_eval do
          default_scope ->{ where(tenant_id: Tenant.current_id) }
        end
      end
    end
    

    All you need to do is set Tenant.current_id to something early in the request, and any table that contains tenant_id will automatically become scoped without any additional code. Instantiating records will automatically inherit the tenant id they were created under.

    The important thing about this use-case is that the scope is set once per request, and it doesn't change. The only cases you will need unscoped here are special cases like background workers that run outside of a request scope.

I am downvoting this example because it is...

Syntax

Syntax

Parameters

Parameters

Remarks

Remarks

Still have a question about Rails Best Practices? Ask Question

Fat Model, Skinny Controller

10

“Fat Model, Skinny Controller” refers to how the M and C parts of MVC ideally work together. Namely, any non-response-related logic should go in the model, ideally in a nice, testable method. Meanwhile, the “skinny” controller is simply a nice interface between the view and model.

In practice, this can require a range of different types of refactoring, but it all comes down to one idea: by moving any logic that isn’t about the response to the model (instead of the controller), not only have you promoted reuse where possible but you’ve also made it possible to test your code outside of the context of a request.

Let’s look at a simple example. Say you have code like this:

def index
  @published_posts = Post.where('published_at <= ?', Time.now)
  @unpublished_posts = Post.where('published_at IS NULL OR published_at > ?', Time.now)
end

You can change it to this:

def index
  @published_posts = Post.published
  @unpublished_posts = Post.unpublished
end

Then, you can move the logic to your post model, where it might look like this:

scope :published, ->(timestamp = Time.now) { where('published_at <= ?', timestamp) }
scope :unpublished, ->(timestamp = Time.now) { where('published_at IS NULL OR published_at > ?', timestamp) }

Convention Over Configuration

9

In Rails you find yourself looking at controllers, views and models for your database.

In order to reduce the need of heavy configuration, Rails implements rules to ease up working with the application. You may define your own rules but for the beginning (and for later on) it's a good idea to stick to conventions that Rails offers.

These conventions will speed up development, keep your code concise and readable and allow you an easy navigation inside your application.

Conventions also lower the barriers of entry for beginners. There are so many conventions in Rails that a beginner doesn’t even need to know about, but can just benefit from in ignorance. It’s possible to create great applications without knowing why everything is the way it is.

For Example

If you have a database table called orders with the primary key id. The matching model is called order and the controller that handles all the logic is named orders_controller. The view is split in different actions: if the controller has a new and edit action, there is also a new and edit view.

For Example

To create an app you simply run rails new app_name. This will generate roughly 70 files and folders that comprise the infrastructure and foundation for your Rails app.

It includes:

  • Folders to hold your models (database layer), controllers, and views
  • Folders to hold unit tests for your application
  • Folders to hold your web assets like Javascript and CSS files
  • Default files for HTTP 400 responses (i.e. file not found)
  • Many others

Beware of default_scope

6

ActiveRecord includes default_scope, to automatically scope a model by default.

class Post
  default_scope ->{ where(published: true).order(created_at: :desc) }
end

The above code will serve posts which are already published when you perform any query on the model.

Post.all # will only list published posts 

That scope, while innocuous-looking, has multiple hidden side-effect that you may not want.

default_scope and order

Since you declared an order in the default_scope, calling order on Post will be added as additional orders instead of overriding the default.

Post.order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."created_at" DESC, "posts"."updated_at" DESC

This is probably not the behavior you wanted; you can override this by excluding the order from the scope first

Post.except(:order).order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."updated_at" DESC

default_scope and model initialization

As with any other ActiveRecord::Relation, default_scope will alter the default state of models initialized from it.

In the above example, Post has where(published: true) set by default, and so new models from Post will also have it set.

Post.new # => <Post published: true>

unscoped

default_scope can nominally be cleared by calling unscoped first, but this also has side-effects. Take, for example, an STI model:

class Post < Document
  default_scope ->{ where(published: true).order(created_at: :desc) }
end

By default, queries against Post will be scoped to type columns containing 'Post'. But unscoped will clear this along with your own default_scope, so if you use unscoped you have to remember to account for this as well.

Post.unscoped.where(type: 'Post').order(updated_at: :desc)

unscoped and Model Associations

Consider a relationship between Post and User

class Post < ApplicationRecord
  belongs_to :user
  default_scope ->{ where(published: true).order(created_at: :desc) }
end

class User < ApplicationRecord
  has_many :posts
end

By getting an individual User, you can see the posts related to it:

user = User.find(1)
user.posts
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' AND "posts"."user_id" = ? ORDER BY "posts"."created_at" DESC [["user_id", 1]]

But you want to clear the default_scope from the posts relation, so you use unscoped

user.posts.unscoped
SELECT "posts".* FROM "posts"

This wipes out the user_id condition as well as the default_scope.

An example use-case for default_scope

Despite all of that, there are situations where using default_scope is justifiable.

Consider a multi-tenant system where multiple subdomains are served from the same application but with isolated data. One way to achieve this isolation is through default_scope. The downsides in other cases become upsides here.

class ApplicationRecord < ActiveRecord::Base
  def self.inherited(subclass)
    super

    return unless subclass.superclass == self
    return unless subclass.column_names.include? 'tenant_id'

    subclass.class_eval do
      default_scope ->{ where(tenant_id: Tenant.current_id) }
    end
  end
end

All you need to do is set Tenant.current_id to something early in the request, and any table that contains tenant_id will automatically become scoped without any additional code. Instantiating records will automatically inherit the tenant id they were created under.

The important thing about this use-case is that the scope is set once per request, and it doesn't change. The only cases you will need unscoped here are special cases like background workers that run outside of a request scope.

Don't Repeat Yourself (DRY)

6

To help to maintain clean code, Rails follows the principle of DRY.

It involves whenever possible, re-using as much code as possible rather than duplicating similar code in multiple places (for example, using partials). This reduces errors, keeps your code clean and enforces the principle of writing code once and then reusing it. It is also easier and more efficient to update code in one place than to update multiple parts of the same code. Thus making your code more modular and robust.

Also Fat Model, Skinny Controller is DRY, because you write the code in your model and in the controller only do the call, like:

# Post model
scope :unpublished, ->(timestamp = Time.now) { where('published_at IS NULL OR published_at > ?', timestamp) } 


# Any controller
def index
    ....
    @unpublished_posts = Post.unpublished
    ....
end

def others
    ...
    @unpublished_posts = Post.unpublished
    ...
end

This also helps lead to an API driven structure where internal methods are hidden and changes are achieved through passing parameters in an API fashion.

You Ain’t Gonna Need it (YAGNI)

1

If you can say “YAGNI” (You ain’t gonna need it) about a feature, you better don’t implement it. There can be saved a lot of development time through focussing onto simplicity. Implementing such features anyway can lead to problems:

Problems

Overengineering

If a product is more complicated than it has to be, it is over engineered. Usually these “not yet used” features will never be used in the intended way they were written and have to be refactored if they ever get used. Premature optimisations, especially performance optimisations, often lead to design decisions which will be proofed wrong in the future.

Code Bloat

Code Bloat means unnecessary complicated code. This can occur for example by abstraction, redundancy or incorrect application of design patterns. The code base becomes difficult to understand, confusing and expensive to maintain.

Feature Creep

Feature Creep refers to adding new features that go beyond the core functionality of the product and lead to an unnecessarily high complexity of the product.

Long development time

The time which could be used to develop necessary features is spent to develop unnecessary features. The product takes longer t deliver.

Solutions

KISS - Keep it simple, stupid

According to KISS, the most systems work the best if they are designed simple. Simplicity should be a primary design goal to reduce complexity. It can be achieved by following the “Single Responsibility Principle” for example.

YAGNI – You Ain’t Gonna Need it

Less is more. Think about every features, is it really needed? If you can think of any way that it’s YAGNI, leave it away. It’s better to develop it when it’s needed.

Continuous Refactoring

The product is being improved steadily. With refactoring we can make sure that the product is being done according to best practice and does not degenerate to a patch work.

Topic Outline