サービスクラスとは?
Railsではビジネスロジックはモデルに実装するのが通説ですが、その原則に従うと、すぐにモデルが肥大してしまいます(いわゆる「Fat Model」)。
そこで肥大化したActiveRecordモデルをリファクタリングする7つの方法で紹介されているように、モデル以外にロジックを分離していきます。
特に「サービス」が便利で、ある一つの単位の機能をまとめて実装します。
オリジナル実装ルール
サービスクラスは以下のルールで実装すると、綺麗に実装できると思います。
- クラス名には動詞と目的語と「Service」を付ける
- 引数は出来る限りnewで渡してインスタンス化する
- 1つのサービスにpublicなメソッドは、原則1つにする
- 初期化したインスタンスはprivateのattr_readerで呼ぶ
- 切り分けたメソッドは全てprivateなgetterメソッドとして実装する
実際のコード例は以下で、元々のコードは先に挙げたページを参考にしています。
# 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
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の提供する機能はモデルに書くべきだと思います。
また、かなり汎用的な処理の場合も、モデルに書くべきだと思います。
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」といったクラスも紹介されています。
こうした特定の条件下で利用できるようなクラスも利用するべきだと思います。