Help us understand the problem. What is going on with this article?

[Rails] Serviceクラスの設計で悩んだこと

RailsでServiceクラスの設計について考えたことを、まとめておきます。
(まだ最適解には至ってないです)

会員登録機能を作る

分かりやすいように例として、よくあるユーザの会員登録機能を実装してみます。
パスワードの暗号化、認証urlの発行、認証メールの送信など、Controllerに全て書くとfatになってしまいそうです。こういう処理の流れは1つの処理の流れとしてどこかにまとめて書いた方が良さそうです。

Serviceクラスの役割とは

自分の中でServiceクラスの役割として、

  • ロジックの集約
  • Controllerの肥大化を防ぐ
  • ユースケースを表現する

があると思っています。会員登録はユースケースとして捉えることができるので、Serviceクラスに当てはまりました。

いざ設計

では、具体的に設計を考えてみます。まず、漠然と呼び出しのイメージを考えてみます。

registration_controller.rb
class RegistrationController < ApplicationController
  def create
    SignUpService.sign_up(email, password, confirm_password)
    if # 成功したかの判定
      redirect_to user_confirm_path
    else
      render 'new'
    end
  end
end

大まかですが呼び出し側は、メソッド1発呼び出して全ての処理ができるといいなと考えました。

次にSerivice側です。
こっちの設計にとても悩みました。

moduleにしてみる

moduleとclassの違いは、インスタンス化できるかどうかです。
処理の塊にしたかっただけなので、インスタンス化する必要はなさそうだと判断し、まずはmoduleに決めました。

sign_up_service.rb
module SignUpService
  def sign_up(email, password, confirm_password)
    validate_params(email, password, confirm_password)
    user_create(email, password) 
    send_confirmation_mail(email)
  end

  private 

  def validate_params(email, password, cofirm_password); end
  def user_create(email, password); end
  def send_confirmation_mail(email); end
end

こんな感じでsign_upメソッドを呼び出すと順々に処理がされるようなイメージになりました。
外からはsign_upメソッドしか見えないので、使い方が分かりやすくて良さそうです。

moduleでの問題点

いざ上記の設計で作っていくと、「状態を持たせたく」なってきました。
例えば、処理が成功したかどうか、エラーメッセージなどです。
moduleでも実現できそうですが、classにしてnewしてインスタンス変数に処理結果を持たせた方がシンプルでいいのではないかと思い始めました。

やっぱclassにしてみる

やっぱclassで大枠を作ってみました。

sign_up_service
class SignUpService
  attr_reader :success, :error_messages

  def initialize(email, password, confirm_password)
    # インスタンス変数へ代入 
  end
  
  def sign_up
    return unless validate_params
    user_create
    send_confirmation_mail
  end

  private

  def validate_params
    if @password != @confirm_password
      error_messages.push('パスワードが一致しません') 
      success = false
      return false
    else
      ...
    end
  end
  def user_create; end
  def send_confirmation_mail; end
end

@success@error_messagesなどのインスタンス変数を持たせ、処理が終わるとそれぞれ結果を代入します。

registration_controller.rb
class RegistrationController < ApplicationController
  def create
    sign_up_service = SignUpService.new(email, password, confirm_password)
    sign_up_service.sign_up
    if sign_up_service.success
      redirect_to user_confirm_path
    else
      @error_messages = sing_up_service.error_messages
      render 'new'
    end
  end
end

呼び出し側では、インスタンスを作成しsign_upメソッドを呼び出した後に、インスタンス変数の中身を取り出すことで処理結果が分かります。使い勝手は良さそうです。
ただ、インスタンス変数への代入が複数箇所に及ぶのが気になるところではあります。また、SignUpServiceでsign_upメソッドを呼び出すのもなんだかなという気がします。

結果

いろんな記事を参考にしつつ、色々迷いがありましたが自分の中でこの形に落ち着きました。
(具体的な処理は省略しています)

sign_up_service
class SignUpService
  attr_reader :success, :error_messages

  def initialize(email, password, confirm_password)
    @email = email
    ... # 省略
    @error_messages = []
  end
  
  def call
    return unless validate_params
    user_create
    send_confirmation_mail
  end

  private

  def validate_params
    if @password != @confirm_password
      error_messages.push('パスワードが一致しません') 
      success = false
      return false
    else
      ...
    end
  end
  def user_create; end
  def send_confirmation_mail; end
end
registration_controller.rb
class RegistrationController < ApplicationController
  def create
    sign_up_service = SignUpService.new(email, password, confirm_password)
    sign_up_service.call
    if sign_up_service.success
      redirect_to user_confirm_path
    else
      @error_messages = sing_up_service.error_messages
      render 'new'
    end
  end
end

まとめるとこのようになります。

Serviceクラス

  • classにする
  • 外から見えるのはcallメソッドのみにする
  • attr_readerで状態を持たせる
  • 処理結果をインスタンス変数に格納するようにする

呼び出し側

  • インスタンス化する
  • インスタンスに対してcallメソッドを呼び出す
  • インスタンスの属性を見て処理をする

処理結果をインスタンス越しに取得できるのが便利になりました。

課題

課題点として

  • 呼び出しがnewしてcallしてと冗長
  • class内でインスタンス変数に代入する処理が散らばっている(代入し忘れや予期せぬ上書きなどが起こりそう)

があげられそうです。まだまだ改善の余地がありそうです。
もっと調べて固めていきたいです。

whisky_daisuky
好きなウイスキーはニッカのフロムザバレルです。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした