RailsのService層とうまく付き合うにはどうすればいいのか調べてみた
「RailsのService層ってどう使っていくのがいいんだろうね?」って聞かれたときにすぐ答えられなかったのでまとめておきたいと思います。
※ Fat Modelの解決策としてTrailblazerが最近よく話題に上がりますが、私がまだ使ったことがないので触れない方向で行きます。
情報を漁る
まずは「Rails service」とググって検索して引っかかった記事を読みました。
- アクションが複雑になる場合 (決算期の終わりに帳簿をクローズする、など)
→ 複雑な処理をmodelから分離させたい- アクションが複数のモデルにわたって動作する場合 (eコマースの購入でOrder, CreditCard, Customer を使用する、など)
→ どのmodelに書けばいいのかよく分からないのでとりあえずserviceに書いとけ感ある- アクションから外部サービスとやりとりする場合 (SNSに投稿する、など)
→ 外部サービスだしServiceっぽい!- アクションが背後のモデルの中核をなすものではない場合 (一定期間ごとに古くなったデータを消去する、など)
→ 複雑な処理をmodelから分離させたい- アクションの実行方法が多岐にわたる場合 (認証をアクセストークンやパスワードで行なう、など)。これはGoF (Gang of Four) のStrategyパターンです。
→ 複雑な処理をmodelから分離させたい
- 一つのModelで複数のミドルウェアと通信する場合はService層に書く
→ 複雑なロジックをmodelから分離したい- 複数のModelが絡み合う処理はService層に書く
→ どのmodelに書けばいいのかよく分からないのでとりあえずserviceに書いとけ感ある- Service層では振る舞いだけを定義する(状態を持たないためModuleで設計する)
→ テストをしやすいようにする、1serviceに責務を負わせすぎないようにする
- クラス名には動詞と目的語と「Service」を付ける
→ ソースを読んだときにすぐServiceだと気づけるようにするため、命名を悩まないようにするため- 引数は出来る限りnewで渡してインスタンス化する
→ 1serviceに1つの役割だけを持たせるため、- 1つのサービスにpublicなメソッドは、原則1つにする
→ 1serviceに1つの機能だけを持たせるため、上と一緒- 初期化したインスタンスはprivateのattr_readerで呼ぶ → なるべく隠蔽したい、attr_readerにまとめることで可読性が上がる
- 切り分けたメソッドは全てprivateなgetterメソッドとして実装する
→ 可読性、隠蔽の面から
- 命名規則を1つに定める 記事では名詞+動詞orだと動詞に違和感がある場合があるので動詞+名詞の方がいいかもって言ってる
→ 可読性- Service Objectを直接インスタンス化しないようにする
→call
をClass Methodとして実装することでステートレスなメソッドとして実装したい- Service Objectの呼び出し方法を1つに定める
→call
,run
,execute
などの意味を持たないメソッドを使うよう義務付けてクラス名でどんな機能なのか表現できるようにする、serviceを利用しようと思ったときに調べなくて済むようになる- Service Objectの責務を1つに絞り込む
Manage
などの責務が曖昧な単語をserviceクラスの命名として使わないよう気をつける
→ 細かい粒度で作って疎にするため- Service Objectのコンストラクタを複雑にしない
→ 複雑なコンストラクタは責務が複雑な証拠。できるだけシンプルなserviceになるように心がける。- callメソッドの引数をシンプルにする 利用しやすいように、引数が2つ以上あるときはキーワード引数を使うとよい
- 結果はステートリーダー経由で返す
call
による副作用を期待するもの(DBに対する更新、メール/Slack送信など)だけではなく処理の結果を受け取りたいときはcall
の返り値をservice objectそのものにすると柔軟な処理がかけて良い。- callメソッドの可読性を下げないようにする
メソッドを分割して可読性を維持する- callメソッドをトランザクションでラップすることを検討する
→ 処理の中断によるバグをなくす- Service Objectが増えたら名前空間でグループ化する
→ 可読性向上
ServiceをCommandパターンで作るのをいい感じにサポートしてくれるGemが紹介されていた。
- commandパターンの為のレールを敷いてくれる
- 引数のvalidationができる
run
とrun!
をいい感じに使い分けることができる- services/ではなくinteractions/として別のディレクトリを切ることを推奨している
- そもそもserviceという曖昧な名前を使うのがよくないのではと思っていたのでこれはよさそう
記事で主張していることが被ってきたのでまとめてみると
Railsでよく使われているServiceの役割
ARを継承したクラス(≒テーブル)を利用しないロジックをまとめる
- 便利ツール群みたいなイメージ
- Gemが提供している機能を使いやすいようにwrapする、外部サービスとの連携など
複雑なロジックをまとめておきcontroller側で簡潔に呼ぶ
- 複雑なロジックなのでCommandパターンを使ってできるだけ分割して書くと色々メリットがある
- 可読性の向上
- ステートレス(
call
のclass method化)にすることにより、テストをしやすくなる
- 複数リソースを扱うために使うのもここに該当
- starategyパターンもここに該当
オープンソースを眺めてみる。
理想はだいたい分かったので実際どんな感じになっているか確かめるためにオープンなRailsアプリケーションはどうなっているのか眺めてみた。
#{動詞}#{目的語}Service
という命名規則- 基本的にServiceクラスのpublic methodは
call
のみ- 綺麗なCommandパターン
call
の見通しがよくなるように処理をprivateメソッドを切り出している- privateメソッド内で他serviceを呼んでいて綺麗に分離できていてすごい
- 便利ツール群はlib/にあった
#{動詞}#{目的語}Service
という命名規則- 機能によってnamaspaceが切られている
- 基本的にServiceクラスのpublic metodは
execute
のみ - レコードオブジェクトを操作するタイプのServiceはレコードオブジェクトをそのまま返していることが多い
- google-apiのwrapはlib/でしていた
- 便利ツール群はlib/に書いてあるみたい
#{名詞}#{動詞}er/or
という命名規則- 色々なメソッドが生えている
- たまにCommandパターンのserviceがあったりする
- 正直よくわからない
- 便利ツール群はlib/にあった
- discourseって意外とイケてない?
まとめ
- Serviceという言葉は曖昧なのでプロジェクトごとでしっかりとルールを決めよう。
- Commandパターンで実装するのか、全部のせServiceクラスを作って実装するのか
- 多くの記事がCommandパターンを用いた実装を推奨
- でも、ある程度の経験がないとどのくらいの粒度で作ればいいのか悩むかも?
- 複雑なロジックを整理するためにServiceを使うときはCommandパターン使って上手に分割しよう。
- Serviceクラスを作るときは
Manage
などの責務の広そうな単語を使わないよう注意する。 - Commandパターンのような一貫したルールがあるのとないのとでは初見でServiceのコードを見たときのインパクトが全然違う。
- Serviceクラスを作るときは
- 今まで便利ツール群はServiceに置いてたけどlib/に置くのがベターっぽい。
追記
色々書きましたが Service層とうまく付き合うには texta.fm の 3. Low-Code Developmentを聞いてもらうのが一番です。聞いたことない人はぜひ聞いてみてください。Service層に対する解像度がぐっと高まると思います。