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.

Introduction

Introduction

expand all collapse all

Examples

  • 24

    “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) }
    
  • 12

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

    To reduce the need for 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 to 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
  • 9

    "Fat Model, Skinny Controller" is a very good first step, but it doesn't scale well once your codebase starts to grow.

    Let's think on the Single Responsibility of models. What is the single responsibility of models? Is it to hold business logic? Is it to hold non-response-related logic?

    No. Its responsibility is to handle the persistence layer and its abstraction.

    Business logic, as well as any non-response-related logic and non-persistence-related logic, should go in service objects.

    Service objects are classes designed to have only one responsibility in the domain of the problem. Let your classes "Scream Their Architecture" for the problems they solve.

    In practice, you should strive towards skinny models, skinny views and skinny controllers. The architecture of your solution shouldn't be influenced by the framework you're choosing.

    For example

    Let's say you're a marketplace which charges a fixed 15% commission to your customers via Stripe. If you charge a fixed 15% commission, that means that your commission changes depending on the order's amount because Stripe charges 2.9% + 30¢.

    The amount you charge as commission should be: amount*0.15 - (amount*0.029 + 0.30).

    Don't write this logic in the model:

    # app/models/order.rb
    class Order < ActiveRecord::Base
      SERVICE_COMMISSION = 0.15
      STRIPE_PERCENTAGE_COMMISSION = 0.029
      STRIPE_FIXED_COMMISSION = 0.30
    
      ...
    
      def commission
        amount*SERVICE_COMMISSION - stripe_commission  
      end
    
      private
    
      def stripe_commission
        amount*STRIPE_PERCENTAGE_COMMISSION + STRIPE_PERCENTAGE_COMMISSION
      end
    end
    

    As soon as you integrate with a new payment method, you won't be able to scale this functionality inside this model.

    Also, as soon as you start to integrate more business logic, your Order object will start to lose cohesion.

    Prefer service objects, with the calculation of the commission completely abstracted from the responsibility of persisting orders:

    # app/models/order.rb
    class Order < ActiveRecord::Base
      ...
      # No reference to commission calculation
    end
    
    # lib/commission.rb
    class Commission
      SERVICE_COMMISSION = 0.15
    
      def self.calculate(payment_method, model)
        model.amount*SERVICE_COMMISSION - payment_commission(payment_method)  
      end
    
      private
    
      def self.payment_commission(payment_method, model)
        # There are better ways to implement a static registry,
        # this is only for illustration purposes.
        Object.const_get("#{payment_method}Commission").calculate(model)
      end
    end
    
    # lib/stripe_commission.rb
    class StripeCommission
      STRIPE_PERCENTAGE_COMMISSION = 0.029
      STRIPE_FIXED_COMMISSION = 0.30
    
      def self.calculate(model)
        model.amount*STRIPE_PERCENTAGE_COMMISSION
          + STRIPE_PERCENTAGE_COMMISSION
      end
    end
    
    # app/controllers/orders_controller.rb
    class OrdersController < ApplicationController
      def create
        @order = Order.new(order_params)
        @order.commission = Commission.calculate("Stripe", @order)
        ...
      end
    end
    

    Using a service object has the following architectural advantages. It:

    • is extremely easy to unit test, as no fixtures or factories are required to instantiate the objects with the logic.
    • works with everything that accepts the message amount.
    • keeps each service object small, with clearly defined responsibilities, and with higher cohesion.
    • easily scales with new payment methods by addition, not modification.
    • stops the tendency to have an ever-growing User object in each Ruby on Rails application.
Please consider making a request to improve this example.

Syntax

Syntax

Parameters

Parameters

Remarks

Remarks

Still have a question about Rails Best Practices? Ask Question

Topic Outline