サービスクラス

クラスの復習をしながらサービスクラスについてまとめる
使用する理由や使用するケースについては深く踏み込まず、文法的な注意点についてまとめる

POROクラス(何も継承しないクラス)として実装する
active modelなどのclass・moduleをinclude・継承しない
値を複数返す必要がある場合はhashで返す。値の返却のためのattributesを定義しない(構造体にしない)
クラス内部で使うためのattributes定義やインスタンス化はしても良い(後述)
class Users::UserFinder
def self.call(arg1: arg1, arg2: arg2)
(略)
end
end
# 使い方 キーワード引数を使ってわかりやすくする。
user = Users::UserFinder.call((arg1: arg1, arg2: arg2)
# 返り値が複数ある場合
class Users::UserFinder
def self.call
{user: user, foo: foo}
end
private
def self.user
(略)
end
def self.foo
(略)
end
end
# 使い方
results = Users::UserFinder.call(arg1: arg1, arg2: arg2)
返り値が複数ある場合はhashで統一する。
このときに、hashではなくattributesが定義された構造体で返すのもありかもしれないが、hashというシンプルな形で返す(callメソッドの返り値に集約する)のが最も読みやすいと思っている。

クラス内が大きくなる場合(内部でインスタンス化する)
class Users::UserFinder
def self.call(user_id)
new(user_id).call
end
def initialize(user_id)
@user_id = user_id
end
def call
foo
bar
end
private
attr_reader :user_id
def foo
# user_id を使う処理
end
def bar
# user_id を使う処理
end
end
# 使い方
user = Users::UserFinder.call(user_id)
この場合、user_idをattr_readerで定義し、使いまわしやすくしている。
シンプルさで言えばクラスメソッドだけで済ませるのには劣るが、引数やprivateメソッドが増えて使いまわしたくなった場合にはこの形がすごく便利に感じるはずだ。
attr_readerはあくまで内部で使うためであり、外部から呼び出す際にどんなattributesが生えているのかは把握しなくても良い形にする。

initializeメソッド
コンストラクタのようなものらしい。
コンストラクタとは?
あまり知識は変わらない
attr_accessorメソッド
値を読み出すメソッドを「ゲッターメソッド」、値を書き込むメソッドを「セッターメソッド」と呼ぶ。
ゲッターメソッドとセッターメソッドを総称して、「アクセサメソッド」と呼ぶ。

class AuthenticateUser
private_class_method :new
def self.call(user, unencrypted_password)
new(user, unencrypted_password).send(:call)
end
private
def initialize(user, unencrypted_password)
@user = user
@unencrypted_password = unencrypted_password
end
def call
return false unless @user
if BCrypt::Password.new(@user.password_digest) == @unencrypted_password
@user
else
false
end
end
end

AuthenticateUser.call
では自身のインスタンスを生成し、同名のインスタンスメソッドを呼び出すことで処理を委譲します。

注意点
- 命名規則を1つに定める
- Service Objectを直接インスタンス化しないこと
- Service Objectの呼び出し方法を1つに定める
- Service Objectの責務を1つに絞り込む
- Service Objectのコンストラクタをシンプルに保つ
- callメソッドの引数をシンプルにする
- 結果はステートリーダー経由で返す

気になった箇所
- Service Objectを直接インスタンス化しないこと
Service Objectをインスタンス化しても、単にcallメソッドを実行する以外にあまり使いみちがありません。
-
Service Objectの責務を1つに絞り込む
Service Objectでありがちなアンチパターンとしては、たとえばManageUserというserviceがユーザーの作成と削除の両方を引き受けてしまうというものがあります。そもそもmanageという言葉だけでは責務があいまいになってしまい、オブジェクトがどんなアクションを実行すべきかを制御する方法も明確になりません。代わりにDeleteUserとCreateUserという具体的な2つのServiceに分けて導入すれば、コードも読みやすくなり、より自然になります。 -
Service Objectのコンストラクタをシンプルに保つ
一般に、実装するほとんどのクラスではコンストラクタをシンプルにしておくのがよい考えです。Serviceの主な呼び出し方法はクラスメソッドを呼び出すことですが、コンストラクタの責務を「引数をServiceのインスタンス変数に保存すること」に限定する方がずっとメリットが多くなります。
class DeleteUser
def initialize(user_id:)
@user_id = user_id
end
def call
#…
end
private
attr_reader :user_id
def user
@user ||= User.find(user_id)
end
end
コンストラクタではなくcallメソッドに集中できるようになりますし、コンストラクタに何を置いてもよく、何を置いてはいけないかという線引きも明確になります。開発者が日々決定しなければならないことはたくさんあるので、こういう点を標準化して決めごとをひとつ減らしましょう。
7.結果はステートリーダー経由で返す
Service Objectはcallメソッドの結果は、たとえば次のように返すことが可能です。実行が成功した場合はtrue、失敗した場合はfalseを返す、という具合です。
しかし、callメソッドがService Object自身を返すようにすれば、もっと柔軟になります。この方法にすれば、Service Objectインスタンスのステートを読み出せるようになります
8.callメソッドの可読性を下げないよう注意する
callメソッドはService Objectの中心となるメソッドです。Service Objectでは、callメソッドをできるだけ読みやすく保つことをおすすめします。callメソッドには関連する手順だけを記述し、それ以外のロジックは最小限に抑えるようにします。andやorなどを使って、特定の手順のフローを制御してもよいでしょう。
class DeleteUser
#…
def call
delete_user_comments
delete_user and
send_user_deletion_notification
end
private
#…
end