とーますメモ

Ruby on Rails / Goなどの学習メモ

【Rails】サービスクラスについて自分なりにまとめてみた

最近、設計した画面が4〜5個のモデルが絡む画面があった。
その時コントローラー内で実装した場合、肥大化することが容易に想像でき、
またどれか一つのモデル内で実装する処理としては複雑なため「サービスクラス」が必要なのではないかと思い調べてみた。

いろいろ調べているとこのページの内容がすごい良かった。
肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

自分のケースの処理だと、入出力とバリデーションに対しての処理がメインになるため
この記事内で言う「Form Object」のパターンで対応することにした。
他にも「ActiveRecord」のオブジェクトを"装飾"して使用するDecoratorパターンは今後使えそう。

しかし、今後サービスクラス(Service Object)のケースを使用するケースが
あるかもしれないのでこの機会に詳細を調べてみる。
色々と検索すると、「サービスクラス」を安易に使用すると、逆にカオスになるので
使用は注意したほうが良いという記事が沢山見つかった。

サービスクラスを使用しようと思うケースは以下のパターンがあると思う。

1)肥大化したモデルのメソッドをサービスクラスに移す

本来モデルが持つべき知識を移しているので、サービスクラス内のコードが再利用しにくい。
この場合、「Value Object」や、同じグループのロジックを切り出したバニラクラスを継承や「コンポジション(または集約)」として使用するケースを
一度考えてみる。そのためにRailsにはdelegateやcomposed_ofなどの便利な機能がある。
単一モデルが肥大化した場合は、サービスクラスは使用しない方が良いと思う。

Railsアンチパターン<モデル編>②Fat model - Qiita
継承のMix-inについて - Kazunori Kamiya - Medium
俺が悪かった。素直に間違いを認めるから、もうサービスクラスとか作るのは止めてくれ - Qiita

2)複数モデルを触るのでサービスクラスを作る

まずは本来モデルが持つべき責務ではないか?、誰が担う責務なのかを考える。
なぜならサービスクラスの責務を強制する仕組みがないため、実装者によっては、まったく意図しないコードができる可能性があるため。
そのため本来モデルにあるべきコードがサービスクラスに実装されてしまう状態が発生する。

ここでサービスクラス内ではなく、モデルクラス内にビジネスロジックを書けばよいのでは?という疑問もあると思うが
自分の場合、サービスクラスを使用する判断としては、「モデルが3つ以上絡む」場合に使用する方針にする。
それモデルが2つ程度で、リレーションがあるモデルなら、責務範囲として、いずれかのモデル内で完結させる。

また独自の計算ロジック、認証、外部API連携、プッシュ通知、メール送信など、「どのモデルにも属さないロジック」がある場合は、
サービスクラス内で書く。

Form Objectとの使い分けは以下

Form Object: 入出力がメインな処理
Serivce Object: ビジネスロジック

サービスクラスを使用する場合は、以下のルールに従って作成

クラス名: #{動詞}#{目的語}Service(#{名詞}#{動詞}er/orという命名方法もあるが、自分は誰が見ても、役割がすぐに分かるServiceを名前に入れる)
メソッド: publichメソッドは、execute(callやrunもあるが、自分はexecuteが好み)の1つのみ。他はprivateメソッドにしてカプセル化する。(単一責任の原則)
配置場所: app/services/内
責務: 1つのビジネスロジックのみ。

Service Objectでありがちなアンチパターンとしては、たとえばManageUserというserviceがユーザーの作成と削除の両方を引き受けてしまうというものがあります。そもそもmanageという言葉だけでは責務があいまいになってしまい、オブジェクトがどんなアクションを実行すべきかを制御する方法も明確になりません。代わりにDeleteUserとCreateUserという2つのserviceに分けて導入すれば、コードも読みやすくなり、より自然になります。

Railsで重要なパターンpart 1: Service Object(翻訳)

以下のモジュールをincludeすることでXXXXService.new.call(params)と呼び出さず、XXXXService.call(params)と呼び出すことができる。
またexecuteの戻り値はサービスクラスのインスタンスを返す。
※以下のモジュールの読み込みもとにもexecuteのインスタンスメソッドが必要

module Service
  extend ActiveSupport::Concern
  class_methods do
    def execute(*args)
      service = new(*args)
   service.execute
      service
    end
  end
end

[参考]
RailsのService層とうまく付き合うにはどうすればいいのか調べてみた - 男女比はカレーと福神漬けと同じくらい
Railsで重要なパターンpart 1: Service Object(翻訳)
Railsプロジェクトにサービスクラスを導入して4ヶ月たったので振り返る - Qiita
Railsでやってしまいがちな保守性を下げてしまうコードとその解決策 - Qiita
Ruby (off|with) the Rails - Speaker Deck

追記

エラー設計の良記事:
blogs.msdn.microsoft.com