LoginSignup
3
4

More than 1 year has passed since last update.

【Rails】Serviceオブジェクトとの付き合い方。

Posted at

Serviceオブジェクトとは

肥大化したActiveRecordモデルを分割し、コントローラをスリムかつ読みやすくするうえで非常に有用な、Ruby on Rails開発における一種の開発パターンです。

どんな時に利用する?

  • 複数のモデルに対するコールバックなどを含み、多くの手順で構成された複雑なアクションで、適切な定義箇所が見つからない場合

  • アクションから外部サービスとやりとりする場合

メリット

  • ModelやControllerがシンプルになり、可読性が向上する

  • 処理がservice層に切り出されている為、Modelのテストが書きやすい

  • Webのインタフェースからビジネスロジックを切り離すことで、責任範囲が明確になる

デメリット

  • 明確なルールが存在しない

  • チームでの認識を合わせるのが難しく、運用が難しい

  • 本質的にServiceオブジェクトパターンそのものには、コードベースを読みやすくする力も、メンテしやすくする能力も、concernをうまく分割する手腕もない

実装例

Serviceオブジェクト

class Staff::Authenticator
  def initialize(staff_member)
    @staff_member = staff_member
  end

  def authenticate(raw_password)
    @staff_member &&
     @staff_member.hashed_password &&
     @staff_member.start_date <= Date.today &&
     (@staff_member.end_date.nil? || @staff_member.end_date > Date.today) &&
     BCrypt::Password.new(@staff_member.hashed_password) == raw_password
  end
end

コントローラー

app/controllers/staff/sessions_controller.rb
class Staff::SessionsController < Staff::StaffBaseController
.
.
.
  def create
    @form = Staff::LoginForm.new(login_form_params)

    if @form.email.present?
      staff_member = StaffMember.find_by("LOWER(email) = ?", @form.email.downcase)
    end

    if Staff::Authenticator.new(staff_member).authenticate(@form.password)
      if staff_member.suspended?
        staff_member.events.create!(type: 'rejected')
        flash.now.alert = 'アカウントが停止されています。'
        render :new
      else
        session[:staff_member_id] = staff_member.id
        session[:last_access_time] = Time.current
        staff_member.events.create!(type: 'logged_in')
        flash.notice = 'ログインしました'
        redirect_to :staff_root
      end
    else
      flash.now.alert = 'メールアドレスまたはパスワードが正しくありません'
      render action: 'new'
    end
  end
.
.
.
end

利用する上での注意点

1. 命名規則を1つに定める

Serviceオブジェクトの場合、UserCreatorやUserAuthenticatorなどのように「〜or」で終わる名前を付ける方法が広く採用されています。この命名規則に従おうとすると、少し無理やり命名しなければいけない場合があります。
そのため、CreateUserやAuthenticateUserのように、コマンドやアクションを先に書く命名方法が責務が明確になりおすすめです。
どんな方法にしろ、命名規則は守りましょう。

2.Serviceオブジェクトを直接インスタンス化しない

Serviceオブジェクトをインスタンス化しても、単にcallメソッドを実行する以外に実はあまり使い道がありません。
callメソッドを実行するのであれば、次のように抽象化してみましょう。

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

3. Serviceオブジェクトの呼び出し方法を1つに統一する

個人的にはcallメソッドを使うのが好みですが、呼び出し方法を統一しておけば、新しいServiceオブジェクトを実装するたびに名前を考える面倒がなくなりますし、他のプログラマーは実装の詳細をチェックしなくてもServiceオブジェクトの使い方をすぐに理解できるという効用もあります。

4. Serviceオブジェクトの責務は1つとする

Serviceオブジェクトにさまざまなアクションをまとめることもできますが、アクションのセットは1種類に限定することで、コードも読みやすくなり、より自然になります。

5. callメソッドの引数はシンプルに

Serviceオブジェクトに2つ以上の引数が与えられる場合、引数をわかりやすくするためにキーワード引数の導入を検討するとよいでしょう。引数が1つの場合であっても、キーワード引数にしておくことで読みやすさが向上するでしょう。

UpdateUser.call(attributes: params[:user], send_notification: false)

6. Serviceオブジェクトが増えたら名前空間でグループ化する

コードをうまく編成するために、名前空間で共通のServiceオブジェクトをグループ化することをおすすめします。グループ化する名前空間は、「外部サービス」や「高レベルの機能」など考えられるどんな基準で決めてもかまいません。ただし、Service Objectの命名規則や配置を読みやすい素直なものにするのが名前空間の主要な目的であることをお忘れなく。規則を1つにしていれば、適切な配置は自然に定まります。不要な選択肢を増やさないようにするのがコツです。

Serviceオブジェクトはアンチパターンなのか?

Serviceオブジェクトをググってみると、「利用しない方がいい」などといった意見が意外と多かったりします。
そういった人たちの言い分は、デメリットに書いた通りです。

Serviceオブジェクトを使わないようにするには?

ServiceオブジェクトではなくconcernとPOROを使ってみましょう。
インターフェイスが改善され、concernが正しく分離され、OOP原則が健全に使われるようになり、コードを把握しやすくなります。

まとめ

自由度が高く、便利な開発パターンであると同時に、デメリットも併せ持っていることで批判的な意見もありました。
実際に採用している現場もあるので、一概に「使っちゃダメ!」とは言えませんが、チーム内で相談しながら運用していけ場いいのかなと思います。

参考

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

Rails:Service層を運用して良かったところ、悪かったところ

Service Objectがアンチパターンである理由とよりよい代替手段(翻訳)

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4