1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-02

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内でインスタンス変数に代入する処理が散らばっている(代入し忘れや予期せぬ上書きなどが起こりそう)

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

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?