Once well defined roles are chosen for each layer of Rails, it becomes obvious that there is no answer about what category of objects should handle app-specific business logic. Controllers are responsible for HTTP, views manage formatting and presentation, and records deal with persistence. There is a big gap between these layers where the actual core piece of the app should live, agnostic of communication or databases.
The cloud with questions marks above is exactly where the hard work of designing a good Rails architecture sits. That is the brain of the app, beyond simply transporting data from the database to the web via HTTP and markups. That is where the rules for permissions, validations, user flows, collaboration with other services, and everything that makes the app unique and useful to people. That is the app’s business logic.
The requirements for how business logic should be structured is familiar: there should be small objects that collaborate with each other through exchange of messages. Each object should have a single responsibility, a limited and concise public interface.
Moreover, these objects should not be coupled with Rails. The layers from the framework are already assigned other tasks related to persistence, presentation, and transport. The business logic should be decoupled from external libraries as much as possible, as it specializes in the universe the app is featured in.
For that, we will introduce new layers to the app stack: repositories, inputs, models, actions, and results. To better explain these concepts, let’s start with a simple example of Rails code: a classic blog app. This code is similar to the Blog example found in the Rails Guides.
In the example, there is one Active Record for the table of articles, with a couple of validations:
# app/models/article.rb
class Article < ApplicationRecord
validates :title, presence: true
validates :body, presence: true, length: { minimum: 10 }
end
Articles Controller interacts with the Article Record to instantiate, fetch, and persist data. This code was generated by the Rails scaffold command:
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
before_action :set_article, only: [:show, :edit, :update, :destroy]
# GET /articles
def index
@articles = Article.all
end
# GET /articles/1
def show
end
# GET /articles/new
def new
@article = Article.new
end
# GET /articles/1/edit
def edit
end
# POST /articles
def create
@article = Article.new(article_params)
if @article.save
redirect_to @article, notice: 'Article was successfully created.'
else
render :new
end
end
# PATCH/PUT /articles/1
def update
if @article.update(article_params)
redirect_to @article, notice: 'Article was successfully updated.'
else
render :edit
end
end
# DELETE /articles/1
def destroy
@article.destroy
redirect_to articles_url, notice: 'Article was successfully destroyed.'
end
private
# Use callbacks to share common setup or constraints between actions.
def set_article
@article = Article.find(params[:id])
end
# Only allow a list of trusted parameters through.
def article_params
params.require(:article).permit(:title, :body)
end
end
Finally, the views invoked by Articles Controller renders instances of Article Record to list their contents, and to generate HTML forms. For example, the index view lists all Articles:
<p id="notice"><%= notice %></p>
<h1>Articles</h1>
<table>
<thead>
<tr>
<th>Title</th>
<th>Body</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @articles.each do |article| %>
<tr>
<td><%= article.title %></td>
<td><%= article.body %></td>
<td><%= link_to 'Show', article %></td>
<td><%= link_to 'Edit', edit_article_path(article) %></td>
<td><%= link_to 'Destroy', article, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Article', new_article_path %>
The new view, for instance, renders an HTML form to allow users to write new Articles:
<h1>New Article</h1>
<%= form_with(model: @article) do |form| %>
<% if @article.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:</h2>
<ul>
<% @article.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :title %>
<%= form.text_field :title %>
</div>
<div class="field">
<%= form.label :body %>
<%= form.text_area :body %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
<%= link_to 'Back', articles_path %>