RailsでServiceクラスを使いたいあなたへ

に公開

RailsでServiceクラスを使いたいあなたへ

はじめに

昔、趣味でWebアプリを作っている時に、いろんなデザインパターンを採用し、大量のコードを書き換えた後に、すべて戻した経験があります。

この記事では、なぜ全て戻したいと思ったか、そしてなぜ「Serviceクラス」は不要だと思ったかについて書こうと思います。

なぜ欲しくなったか

開発が進むにつれて以下のような課題に直面し、app/services という「新しい層」が欲しくなりました。

1. 複数のモデルを跨ぐ橋渡しが欲しかった

例えばECサイトで「商品が購入され、決済があり、購入履歴を作り、ユーザーの購入数によってユーザのランクを変更する」という処理があるとします。こういった処理のまとまりをどこか「中立的な場所」に置きたくなりました。

2. サービスの核となるモデルの肥大化を防ぎたかった

先ほどの例でいうと、中心となる ProductUser モデルに周辺ロジックが集中し、肥大化(Fat Model)して「触るとどこが壊れるかわからない」状態になるのを避けたいと考えました。

3. Fat Controllerを避けたかった

「Controller は Service を呼んでレスポンスを返すだけ」というルールに統一すれば、どこに何が書いてあるか迷わなくなり、コードもスッキリして読みやすくなるだろうと考えました。

なぜやめたか

実際にServiceクラスを導入して運用してみると、期待していた「スッキリ感」よりも、以下のような「つらさ」が勝ってしまいました。

ドメイン知識がモデルから逃げてしまった

「商品を購入する」「購入数でランクを判定する」というのは、そのシステムの**ドメイン知識(ビジネスルール)**です。

これらを Service に書いた結果、モデルがただのデータの器になってしまいました。モデルを見てもビジネスルールが分からず、「結局 Service を読まないと何も分からない」状態になりました。

ServiceとModelの境界を自分でも説明できなくなった

最初は「手順だけ」を Service に書くつもりでしたが、実際にはその手順の中に「ルール」も入り込んでしまいます。「どこまでを Model に書き、どこからを Service に書くべきか」の判断が難しく、結局どちらにも書けそうで悩むことが増えました。

どうしたか

最終的に app/services を削除し、すべての処理を app/models に戻しました。
ただし、単にモデルクラスに全部詰め込むのではなく、以下のような手法で整理しました。

Concernsの利用

特定の責務(例: ランク管理、検索、公開状態の管理など)をConcernとして切り出し、コードの見通しを維持する。

app/models/user.rb
class User < ApplicationRecord
  include Rankable
  has_many :orders
end
app/models/concerns/user/rankable.rb
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 に配置しました。

例えば、注文と決済を含む一連の処理はこのように表現できます。

app/models/order_completion.rb
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クラスを作ったけど、なんかしんどい」と感じている方がいたら、この記事が何かのヒントになれば嬉しいです。

参考

Politive Tech Blog

Discussion