Quantcast
Channel: Ruby on Railsの記事一覧|TechRacho by BPS株式会社
Viewing all articles
Browse latest Browse all 1406

Rails5「中級」チュートリアル(3-7)投稿機能: Service Object(翻訳)

$
0
0

概要

概要

原著者の許諾を得て翻訳・公開いたします。

Rails5中級チュートリアルはセットアップが短めで、RDBMSにはPostgreSQL、テストにはRSpecを用います。
原文が非常に長いので分割します。章ごとのリンクは順次追加します。

注意: Rails中級チュートリアルは、Ruby on Railsチュートリアル(https://railstutorial.jp/)(Railsチュートリアル)とは著者も対象読者も異なります。

目次

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"

関連記事

Railsで重要なパターンpart 1: Service Object(翻訳)

新しいRailsフロントエンド開発(1)Asset PipelineからWebpackへ(翻訳)


Viewing all articles
Browse latest Browse all 1406

Latest Images

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

Trending Articles



Latest Images

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭

赤坂中華 わんたん亭