RailsでServiceクラスを使いたいあなたへ
RailsでServiceクラスを使いたいあなたへ
はじめに
昔、趣味でWebアプリを作っている時に、いろんなデザインパターンを採用し、大量のコードを書き換えた後に、すべて戻した経験があります。
この記事では、なぜ全て戻したいと思ったか、そしてなぜ「Serviceクラス」は不要だと思ったかについて書こうと思います。
なぜ欲しくなったか
開発が進むにつれて以下のような課題に直面し、app/services という「新しい層」が欲しくなりました。
1. 複数のモデルを跨ぐ橋渡しが欲しかった
例えばECサイトで「商品が購入され、決済があり、購入履歴を作り、ユーザーの購入数によってユーザのランクを変更する」という処理があるとします。こういった処理のまとまりをどこか「中立的な場所」に置きたくなりました。
2. サービスの核となるモデルの肥大化を防ぎたかった
先ほどの例でいうと、中心となる Product や User モデルに周辺ロジックが集中し、肥大化(Fat Model)して「触るとどこが壊れるかわからない」状態になるのを避けたいと考えました。
3. Fat Controllerを避けたかった
「Controller は Service を呼んでレスポンスを返すだけ」というルールに統一すれば、どこに何が書いてあるか迷わなくなり、コードもスッキリして読みやすくなるだろうと考えました。
なぜやめたか
実際にServiceクラスを導入して運用してみると、期待していた「スッキリ感」よりも、以下のような「つらさ」が勝ってしまいました。
ドメイン知識がモデルから逃げてしまった
「商品を購入する」「購入数でランクを判定する」というのは、そのシステムの**ドメイン知識(ビジネスルール)**です。
これらを Service に書いた結果、モデルがただのデータの器になってしまいました。モデルを見てもビジネスルールが分からず、「結局 Service を読まないと何も分からない」状態になりました。
ServiceとModelの境界を自分でも説明できなくなった
最初は「手順だけ」を Service に書くつもりでしたが、実際にはその手順の中に「ルール」も入り込んでしまいます。「どこまでを Model に書き、どこからを Service に書くべきか」の判断が難しく、結局どちらにも書けそうで悩むことが増えました。
どうしたか
最終的に app/services を削除し、すべての処理を app/models に戻しました。
ただし、単にモデルクラスに全部詰め込むのではなく、以下のような手法で整理しました。
Concernsの利用
特定の責務(例: ランク管理、検索、公開状態の管理など)をConcernとして切り出し、コードの見通しを維持する。
class User < ApplicationRecord
include Rankable
has_many :orders
end
module User::Rankable
extend ActiveSupport::Concern
def update_rank_by_order_count
new_rank = calculate_rank
update!(rank: new_rank)
end
private
def calculate_rank
case orders.count
when 50.. then "GOLD"
when 10.. then "SILVER"
else "BRONZE"
end
end
end
PORO (Plain Old Ruby Object) の導入
モデルはデータベースのテーブルだけでなく、ドメインの概念を表現するものです。複数モデルを跨ぐ処理には、それにふさわしいクラスを定義し、純粋なRubyクラスとして app/models に配置しました。
例えば、注文と決済を含む一連の処理はこのように表現できます。
class OrderCompletion
def initialize(user:, product:)
@user = user
@product = product
end
def execute
Order.transaction do
order = @user.orders.create!(product: @product)
order.pay!
@user.update_rank_by_order_count
order
end
end
end
これにより、個々のモデルクラスを太らせることなく、Railsのデフォルト構成の中でロジックを整理することができました。
まとめ
この経験を通して感じたのは、Railsの標準構成はよくできているということです。
app/services という新しい層を作る前に、適切なモデルとその責務を考えることが、我々の作るアプリケーションをより健全にしていくのだと思いました。
最後に
これは私の経験に基づく一つの意見です。Serviceクラスを使っているプロダクトや、それが合っている開発スタイルもあると思います。
もし「Serviceクラスを作ったけど、なんかしんどい」と感じている方がいたら、この記事が何かのヒントになれば嬉しいです。
Discussion