こんにちは、@hairgaiです。
今回は、賛否両論あるServiceクラスについての自分的な使い方を書いていこうと思います。
Serviceクラスとは
DDD(ドメイン駆動設計)でのサービスから派生している(と勝手に認識している)、ある一つの機能を記述するクラス郡です。
詳しくは説明している人がたくさんいらっしゃるので割愛しますが、ビジネスロジックをモデルとコントローラーの中間でキレイに書けるので、僕はよく使っています。
今回は(僕の使い方は間違ってるかもしれませんが)、自分的な使い方及びそのメリットと思われる部分を書いていきます。
基本的な使い方
まず、基本的な使い方を、コード例と共に紹介しようかなと思います。(これが正解かどうかは正直わかりませんが、見やすいのでいいかなと思ってます)
例えば、SNSなどで「フォローをする」という機能をService層として1ファイルに記述すると、こんな感じにになるかと思います。
※重ねていいますが、これが正解とかじゃないです。
class FollowService
attr_reader :user, :target_user
attr_accessor :follow
def initialize(user, target_user)
@user = user
@target_user = target_user
end
def perform
check!
create_follow!
run_after_worker!
end
private
def check!
check_following!
check_blocking!
end
def check_following!
return true unless user.following?(target_user)
raise ArgumentError, 'User following target user'
end
def check_blocking!
return true unless user.blocking?(target_user)
raise ArgumentError, 'User blocking target user'
end
def create_follow!
self.follow = user.follows.create!(target_user: target_user)
end
def run_after_worker!
AfterFollowWorker.perform_in(0.2.seconds, follow.id)
end
end
ピュアなRubyのクラスで作る
基本的に何かGem等を使って作ることは、僕はしていないです。
ピュアRubyでの実装にすることで「実装の理解に対する障壁を下げる」効果を狙っています。
Serviceクラスは(重い機能になると)ロジックが複雑になりがちなので、なるべくシンプルに作成し、誰が見てもすぐに理解できるように心がけています。
クラス名を定める
命名に好みがあると思いますが、機能を象徴するクラスであるので[動詞]([目的語])Service
で統一しています。
命名規則をつけることで機能が推論しやすくなるというメリットがあります。
publicなメソッドはperformのみにする
ここらへんも好みがあると思います(call
とかにしたりする人も多いです)が、基本的にはpublicなメソッドを一つだけ生やし、それ以外は呼べないものとします。
これは、Serviceクラスは単一の機能を象徴するクラスであり、それを使用することで実現できる機能を単一のものと限定するためです。
この単一のpublicメソッドは結構いろいろな方が言っていますが、僕も設計時点において迷いが全くなくなり実装スピードが格段に上がったので採用しています。
publicなメソッドで呼ぶのはprivateメソッドのみ
これも見通しが良くなる + 1メソッドごとの責務が軽くなり、ガード節が使いやすくなったりするので採用しています。
ここらへんは好みの分かれるところだと思います。
後処理等へのアプローチを単一にする
上のFollowServiceでも書いていますが、業務で使用する以上はユーザさんに対するレスポンスを一番に考えます。
そういった場合のアプローチとしては「1レスポンス中には必要な処理のみを行う」というものがあり、後続処理などはジョブとしてキューに格納し、ジョブサーバ等に処理させることになります。
その際に、ControllerやModel等色々な場所にジョブをコールする処理が散らばると、プロジェクト全体での見通しが悪くなります。
そこで、HogeService
の後処理のジョブはAfterHogeJob
にする、というような命名規則にし、Serviceクラスでのみコールするという決まりにすることで、全体の見通しを良くすることができます。
Controllerの肥大化の解消
言うまでもないですね。
class FollowsController < ApplicationController
def create
target_user = user.find(params[:target_user_id])
service = FollowService.new(current_user, target_user)
service.perform
redirect_to user_path(target_user)
end
end
Modelの肥大化の解消
こちらも言うまでもないですね。
Modelに書いていたものを全部Serviceクラスに持っていき、モデルがデータの関連付けやバリデーション、その他単一モデルに関するメソッド等のみになります。
そのため、モデルからロジックの多いメソッドがなくなり、肥大化した見づらいモデルというものがなくなります。
あとがき
こうして改めて書いてみると、僕は色々と責任をServiceクラスに負わせている書き方をしているんだなぁ、と思います。
Serviceクラスは必要ない、等々議論の余地はあると思いますが、こういった設計等の話はあくまで手法の一つなので、自分たちのビジネスに合わせて適切に使用できると良いですね。