LoginSignup
253
200

More than 5 years have passed since last update.

Railsにおけるサービスクラスのオリジナルルール

Last updated at Posted at 2016-07-12

サービスクラスとは?

Railsではビジネスロジックはモデルに実装するのが通説ですが、その原則に従うと、すぐにモデルが肥大してしまいます(いわゆる「Fat Model」)。
そこで肥大化したActiveRecordモデルをリファクタリングする7つの方法で紹介されているように、モデル以外にロジックを分離していきます。
特に「サービス」が便利で、ある一つの単位の機能をまとめて実装します。

オリジナル実装ルール

サービスクラスは以下のルールで実装すると、綺麗に実装できると思います。

  1. クラス名には動詞と目的語と「Service」を付ける
  2. 引数は出来る限りnewで渡してインスタンス化する
  3. 1つのサービスにpublicなメソッドは、原則1つにする
  4. 初期化したインスタンスはprivateのattr_readerで呼ぶ
  5. 切り分けたメソッドは全てprivateなgetterメソッドとして実装する

実際のコード例は以下で、元々のコードは先に挙げたページを参考にしています。

app/services/user_authenticate_service.rb
# 1. クラス名には動詞と目的語と「Service」を付ける
class UserAuthenticateService
  # 2. 引数は出来る限りnewで渡し、initializeでインスタンス化する
  def initialize(user, unencrypted_password)
    @user = user
    @unencrypted_password = unencrypted_password
  end

  # 3. 1つのサービスにpublicなメソッドは、原則1つにする
  def authenticate
    return false unless user
    user_password == unencrypted_password
  end

  private

  # 4. 初期化インスタンスはprivate以下のattr_readerで呼ぶ
  attr_reader :user, :unencrypted_password

  # 5. getterメソッドとして実装する
  def user_password
    @user_password ||= -> do
      BCrypt::Password.new(user.password_digest)
    end.call
  end
end
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def create
    user = User.find_by(email: params[:email])

    # 引数をnewで渡しインスタンス化
    authenticate_service = UserAuthenticateService.new(user, params[:password])
    if authenticate_service.authenticate
      current_user = user
      redirect_to dashboard_path
    else
      flash[:alert] = 'Login failed.'
      render 'new'
    end
  end
end

各ルールに関して

1. クラス名には動詞と目的語と「Service」を付ける

「Service」を付けるのは単に、サービスクラスを呼び出した時に、それがservicesディレクトリ以下のファイルであることがすぐ分かるようにするためです。
また命名の指針として、動詞と目的語を入れるようにすると分かりやすく、命名にも迷わなくなります。

2. 引数は出来る限りnewで渡し、initializeでインスタンス化する

基本的に引数は最初にだけ渡して、publicに利用するメソッドには引数は渡さないようにしています。

これはルール3とも関連しますが「サービスクラスは原則1つの役割に徹するべき」と考えています。
この原則に従えば、引数はnewで最初の1回だけ渡せば十分になるはずなので、そのように統一しています。
このようなルールで統一すると、メソッドの設計において迷うことが少なくなる思います。

ただし例外としては、サービスクラスのインスタンスを一度生成した後、繰り返し処理を行いたい時です。
そのような場合にのみ、ルール3のpublicなメソッドにも引数を持たせることになります。

3. 1つのサービスにpublicなメソッドは、原則1つにする

繰り返しになりますが「サービスクラスは原則1つの役割に徹するべき」と考えています。
この原則に従えば、外部から呼び出せるpublicなメソッドは1つだけ実装されることになります。
このようなルールに統一しておくことで、クラス設計においても迷うことが少なくなります。

とはいえ、

  • 内容的な重複が多い複数の機能が必要な場合
  • コントローラーの1つのメソッド内でstartとfinishのように使いたい場合

といったケースでは、複数のメソッドを追加することもやむをえないと思います。
ただ、そのような場合でも、あまり数を増やしすぎることは避けるべきだと思います。

4. 初期化インスタンスはprivate以下のattr_readerで呼ぶ

初期化したインスタンスは原則外部から呼び出すことは無いはずなので、privateにするようにしています。

外部から呼び出さないメソッドは全てprivateにしておくことは徹底しています。
Rubyの言語仕様としてそうあるべきだし、可読性も上がります。

5. 切り分けたメソッドは全てprivateなgetterメソッドとして実装する

1つのpublicなメソッドから、適当な粒度でメソッドに切り分けます。
それらはgetterメソッドとして実装し、一度実行されるとその値が保存されるようにしておきます。
今回挙げた例ではその恩恵は無いですが、例えば処理の中で何度もuser_passwordが利用される場合では、再計算する必要が無いので効率的なメソッドになります。

寄せられた疑問・その他追記事項

「サービスクラスは原則1つの役割に徹するべき」理由

これには2つ理由があります。

1つ目は、設計において迷う時間を減らせるということです。
複数のメソッドが必要になったら別のサービスを用意するといった意思決定を迅速に行えるようになりますし、メソッドの引数の設計でも同様にすぐに意思決定できます。

2つ目は「1つのファイルに多数のコードを書くよりは、複数のファイルに分割されている方が可読性が上がる」と感じる経験を多数、してきたからです。
例えばUserモデルのようなよく使われるモデルに、次々とメソッドが追加されて、カオスになって意味が分からなくなるという経験をしました。
それよりは、多少分割しすぎだとしても、別ファイルに分かれている方がずっと良いと思います。

この2つを一般的に実現させられる考え方が「サービスクラスは原則1つの役割に徹するべき」というものです。
ただ、これは原則であって、どうしてもこの原則に従うと不自然になるようなケースでは複数のメソッドを追加することがあっても良いと思います。

どこまでをサービスクラスに実装するのか

基本的に、ほとんど全てのコードをサービスに書けば良いと思っています。

例外として、scopeやvalidationのような、ActiveRecordの提供する機能はモデルに書くべきだと思います。
また、かなり汎用的な処理の場合も、モデルに書くべきだと思います。

app/models/user.rb
class User < ActiveRecord::Base
  # scopeやvalidation
  validates_uniqueness_of :email
  scope :search_by_email, -> (email) { where("email LIKE ?", "%#{email}%") }

  # 汎用的な処理
  def full_name
    first_name + last_name
  end
end

また先に挙げたページで「Formオブジェクト」や「Decorator」といったクラスも紹介されています。
こうした特定の条件下で利用できるようなクラスも利用するべきだと思います。

253
200
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
253
200