RailsでServiceクラスの設計について考えたことを、まとめておきます。
会員登録機能を作る
分かりやすいように例として、よくあるユーザの会員登録機能を実装してみます。
パスワードの暗号化、認証urlの発行、認証メールの送信など、Controllerに全て書くとfatになってしまいそうです。こういう処理の流れは1つの処理の流れとしてどこかにまとめて書いた方が良さそうです。
Serviceクラスの役割とは
自分の中でServiceクラスの役割として、
- ロジックの集約
- Controllerの肥大化を防ぐ
- ユースケースを表現する
があると思っています。会員登録はユースケースとして捉えることができるので、Serviceクラスに当てはまりました。
設計
では、具体的に設計を考えてみます。まず、漠然と呼び出しのイメージを考えてみます。
class RegistrationController < ApplicationController
def create
SignUpService.sign_up(email, password, confirm_password)
if # 成功したかの判定
redirect_to user_confirm_path
else
render 'new'
end
end
end
大まかですが呼び出し側は、メソッド1発呼び出して全ての処理ができるといいなと考えました。
次にSerivice側です。
こっちの設計にとても悩みました。
moduleにしてみる
moduleとclassの違いは、インスタンス化できるかどうかです。
処理の塊にしたかっただけなので、インスタンス化する必要はなさそうだと判断し、まずはmoduleに決めました。
module SignUpService
def sign_up(email, password, confirm_password)
validate_params(email, password, confirm_password)
user_create(email, password)
send_confirmation_mail(email)
end
private
def validate_params(email, password, cofirm_password); end
def user_create(email, password); end
def send_confirmation_mail(email); end
end
こんな感じでsign_upメソッドを呼び出すと順々に処理がされるようなイメージになりました。
外からはsign_upメソッドしか見えないので、使い方が分かりやすくて良さそうです。
moduleでの問題点
いざ上記の設計で作っていくと、「状態を持たせたく」なってきました。
例えば、処理が成功したかどうか、エラーメッセージなどです。
moduleでも実現できそうですが、classにしてnewしてインスタンス変数に処理結果を持たせた方がシンプルでいいのではないかと思い始めました。
classにしてみる
やっぱclassで大枠を作ってみました。
class SignUpService
attr_reader :success, :error_messages
def initialize(email, password, confirm_password)
# インスタンス変数へ代入
end
def sign_up
return unless validate_params
user_create
send_confirmation_mail
end
private
def validate_params
if @password != @confirm_password
error_messages.push('パスワードが一致しません')
success = false
return false
else
...
end
end
def user_create; end
def send_confirmation_mail; end
end
@success
や@error_messages
などのインスタンス変数を持たせ、処理が終わるとそれぞれ結果を代入します。
class RegistrationController < ApplicationController
def create
sign_up_service = SignUpService.new(email, password, confirm_password)
sign_up_service.sign_up
if sign_up_service.success
redirect_to user_confirm_path
else
@error_messages = sing_up_service.error_messages
render 'new'
end
end
end
呼び出し側では、インスタンスを作成しsign_upメソッドを呼び出した後に、インスタンス変数の中身を取り出すことで処理結果が分かります。使い勝手は良さそうです。
ただ、インスタンス変数への代入が複数箇所に及ぶのが気になるところではあります。また、SignUpServiceでsign_upメソッドを呼び出すのもなんだかなという気がします。
結果
いろんな記事を参考にしつつ、色々迷いがありましたが自分の中でこの形に落ち着きました。
(具体的な処理は省略しています)
class SignUpService
attr_reader :success, :error_messages
def initialize(email, password, confirm_password)
@email = email
... # 省略
@error_messages = []
end
def call
return unless validate_params
user_create
send_confirmation_mail
end
private
def validate_params
if @password != @confirm_password
error_messages.push('パスワードが一致しません')
success = false
return false
else
...
end
end
def user_create; end
def send_confirmation_mail; end
end
class RegistrationController < ApplicationController
def create
sign_up_service = SignUpService.new(email, password, confirm_password)
sign_up_service.call
if sign_up_service.success
redirect_to user_confirm_path
else
@error_messages = sing_up_service.error_messages
render 'new'
end
end
end
まとめるとこのようになります。
Serviceクラス
- classにする
- 外から見えるのはcallメソッドのみにする
- attr_readerで状態を持たせる
- 処理結果をインスタンス変数に格納するようにする
呼び出し側
- インスタンス化する
- インスタンスに対してcallメソッドを呼び出す
- インスタンスの属性を見て処理をする
処理結果をインスタンス越しに取得できるのが便利になりました。
課題
課題点として
- 呼び出しがnewしてcallしてと冗長
- class内でインスタンス変数に代入する処理が散らばっている(代入し忘れや予期せぬ上書きなどが起こりそう)
があげられそうです。まだまだ改善の余地がありそうです。
もっと調べて固めていきたいです。