概要
概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: The Ultimate Intermediate Ruby on Rails Tutorial: Let’s Create an Entire App!
- 原文公開日: 2017/12/17
- 著者: Domantas G
Rails5中級チュートリアルはセットアップが短めで、RDBMSにはPostgreSQL、テストにはRSpecを用います。
原文が非常に長いので分割します。章ごとのリンクは順次追加します。
注意: Rails中級チュートリアルは、Ruby on Railsチュートリアル(https://railstutorial.jp/)(Railsチュートリアル)とは著者も対象読者も異なります。
目次
- 1. 序章とセットアップ
- 2. レイアウト
- 3. 投稿
- 3-1 認証
- 3-2 ヘルパー
- 3-3 テスト
- 3-4 メインフィード
- 3-5 単一の投稿
- 3-6 特定のブランチ
- 3-7 Service Object(本セクション)
- 3-8 新しい投稿を作成する
- 4. インスタントメッセージ
- 4-1 非公開チャット
- 4-1-1 前編
- 4-1-2 後編
- 4-2 連絡先
- 4-3 グループチャット
- 4-4 メッセンジャー
- 4-1 非公開チャット
- 5. 通知
- 5-1 つながりリクエスト
- 5-2 チャット
Rails5「中級」チュートリアル(3-7)投稿機能: Service Object(翻訳)
前述のとおり、コントローラ内にロジックを配置するとあっという間に複雑になってしまい、テストが複雑になってしまいます。そういうわけで、こうしたロジックを他の場所に切り出すのはよい考えです。私はそのためにデザインパターンを用いています。具体的にはService Objectと呼ばれるデザインパターンです(単にServiceとも呼ばれます)。
現時点のPostsController
には以下のメソッドがあります(Gist)。
# controllers/posts_controller.rb
def get_posts
branch = params[:action]
search = params[:search]
category = params[:category]
if category.blank? && search.blank?
posts = Post.by_branch(branch).all
elsif category.blank? && search.present?
posts = Post.by_branch(branch).search(search)
elsif category.present? && search.blank?
posts = Post.by_category(branch, category)
elsif category.present? && search.present?
posts = Post.by_category(branch, category).search(search)
else
end
end
Serviceを使ってこの大量の条件ロジックを取り除きたいと思います。Service Object(Service)デザインパターンは、単なる基本的なRubyのクラスです。Service Objectは、処理したいデータをこれに渡して、定義済みのメソッドを呼び出し、欲しい戻り値を受け取るという非常にシンプルなものです。
RubyではClassのinitialize
メソッドにデータを渡します。これは他の言語で言う「コンストラクタ」に相当します。そしてクラス内で、定義済みのすべてのロジックを扱うメソッドを作成します。実際に作ってコードの様子を見てみましょう。
app
ディレクトリの下にservices
ディレクトリを作成します。
app/services
このディレクトリの下にposts_for_branch_service.rb
ファイルを以下の内容で作成します(Gist)。
# services/posts_for_branch_service.rb
class PostsForBranchService
def initialize(params)
@search = params[:search]
@category = params[:category]
@branch = params[:branch]
end
# get posts depending on the request
def call
if @category.blank? && @search.blank?
posts = Post.by_branch(@branch).all
elsif @category.blank? && @search.present?
posts = Post.by_branch(@branch).search(@search)
elsif @category.present? && @search.blank?
posts = Post.by_category(@branch, @category)
elsif @category.present? && @search.present?
posts = Post.by_category(@branch, @category).search(@search)
else
end
end
end
前述したように、これはRubyの普通のクラスであり、パラメータを受け取るinitialize
メソッドと、ロジックを扱うcall
メソッドがあります。このロジックは、get_posts
から持ってきたものです。
後は、get_posts
メソッド内でこのクラスのオブジェクトを作成し、call
メソッドで呼び出します。get_posts
メソッドは次のような感じになります(Gist)。
# controllers/posts_controller.rb
def get_posts
PostsForBranchService.new({
search: params[:search],
category: params[:category],
branch: params[:action]
}).call
end
変更をcommitします。
git add -A
git commit -m "Create a service object to extract logic
from the get_posts method"
spec
Serviceなどのデザインパターンのありがたい点は、単体テストが書きやすいことです。call
メソッドのspecを書いて条件ごとにテストすればよいのです。
spec
ディレクトリの下にservices
ディレクトリを作成します。
spec/services
そのディレクトリの下に、posts_for_branch_service_spec.rb
ファイルを以下の内容で作成します(Gist)。
# spec/services/posts_for_branch_service_spec.rb
require 'rails_helper'
require './app/services/posts_for_branch_service.rb'
describe PostsForBranchService do
context '#call' do
let(:not_included_posts) { create_list(:post, 2) }
let(:category) { create(:category, branch: 'hobby', name: 'arts') }
let(:post) do
create(:post,
title: 'a very fun post',
category_id: category.id)
end
it 'returns posts filtered by a branch' do
not_included_posts
category
included_posts = create_list(:post, 2, category_id: category.id)
expect(PostsForBranchService.new({branch: 'hobby'}).call).to(
match_array included_posts
)
end
it 'returns posts filtered by a branch and a search input' do
not_included_posts
category
included_post = [] << post
expect(PostsForBranchService.new({branch: 'hobby', search: 'fun'}).call).to(
eq included_post
)
end
it 'returns posts filtered by a category name' do
not_included_posts
category
included_post = [] << post
expect(PostsForBranchService.new({branch: 'hobby', category: 'arts'}).call).to(
eq included_post
)
end
it 'returns posts filtered by a category name and a search input' do
not_included_posts
category
included_post = [] << post
expect(PostsForBranchService.new({name: 'arts',
search: 'fun',
branch: 'hobby'}).call).to eq included_post
end
end
end
このファイルの冒頭でposts_for_branch_service.rb
ファイルが読み込まれ、call
メソッドの各条件がテストされます。
変更をcommitします。
git add -A
git commit -m "Add specs for the PostsForBranchService"