Open9

サービスクラス

philosophynotephilosophynote

クラスの復習をしながらサービスクラスについてまとめる

使用する理由や使用するケースについては深く踏み込まず、文法的な注意点についてまとめる

philosophynotephilosophynote

https://zenn.dev/ysi831/books/66ba06d6a4a1d4/viewer/826699#そもそもサービスクラスとは

POROクラス(何も継承しないクラス)として実装する
active modelなどのclass・moduleをinclude・継承しない
値を複数返す必要がある場合はhashで返す。値の返却のためのattributesを定義しない(構造体にしない)
クラス内部で使うためのattributes定義やインスタンス化はしても良い(後述)

service.rb

class Users::UserFinder
  def self.call(arg1: arg1, arg2: arg2)
    (略)
  end
end

# 使い方 キーワード引数を使ってわかりやすくする。
user = Users::UserFinder.call((arg1: arg1, arg2: arg2)

#   返り値が複数ある場合

class Users::UserFinder
  def self.call
    {user: user, foo: foo}
  end

  private

  def self.user
    (略)
  end

  def self.foo
    (略)
  end
end

# 使い方
results = Users::UserFinder.call(arg1: arg1, arg2: arg2)

返り値が複数ある場合はhashで統一する。
このときに、hashではなくattributesが定義された構造体で返すのもありかもしれないが、hashというシンプルな形で返す(callメソッドの返り値に集約する)のが最も読みやすいと思っている。

philosophynotephilosophynote

クラス内が大きくなる場合(内部でインスタンス化する)

service.rb

class Users::UserFinder
  def self.call(user_id)
    new(user_id).call
  end

  def initialize(user_id)
    @user_id = user_id
  end

  def call
    foo
    bar
  end

  private

  attr_reader :user_id

  def foo
    # user_id を使う処理
  end

  def bar
    # user_id を使う処理
  end
end

# 使い方
user = Users::UserFinder.call(user_id)

この場合、user_idをattr_readerで定義し、使いまわしやすくしている。
シンプルさで言えばクラスメソッドだけで済ませるのには劣るが、引数やprivateメソッドが増えて使いまわしたくなった場合にはこの形がすごく便利に感じるはずだ。
attr_readerはあくまで内部で使うためであり、外部から呼び出す際にどんなattributesが生えているのかは把握しなくても良い形にする。

philosophynotephilosophynote

initializeメソッド

コンストラクタのようなものらしい。
コンストラクタとは?

https://wa3.i-3-i.info/word13646.html

あまり知識は変わらない

attr_accessorメソッド

値を読み出すメソッドを「ゲッターメソッド」、値を書き込むメソッドを「セッターメソッド」と呼ぶ。
ゲッターメソッドとセッターメソッドを総称して、「アクセサメソッド」と呼ぶ。

philosophynotephilosophynote

https://qiita.com/QUANON/items/5ef803988c0ad6930e4b#_reference-5ac4a883b01430cd9556

AuthenticateUser

class AuthenticateUser
  private_class_method :new

  def self.call(user, unencrypted_password)
    new(user, unencrypted_password).send(:call)
  end

  private

  def initialize(user, unencrypted_password)
    @user = user
    @unencrypted_password = unencrypted_password
  end

  def call
    return false unless @user

    if BCrypt::Password.new(@user.password_digest) == @unencrypted_password
      @user
    else
      false
    end
  end
end
philosophynotephilosophynote

AuthenticateUser.call では自身のインスタンスを生成し、同名のインスタンスメソッドを呼び出すことで処理を委譲します。

philosophynotephilosophynote

https://techracho.bpsinc.jp/hachi8833/2022_03_17/46482

注意点

  1. 命名規則を1つに定める
  2. Service Objectを直接インスタンス化しないこと
  3. Service Objectの呼び出し方法を1つに定める
  4. Service Objectの責務を1つに絞り込む
  5. Service Objectのコンストラクタをシンプルに保つ
  6. callメソッドの引数をシンプルにする
  7. 結果はステートリーダー経由で返す
philosophynotephilosophynote

気になった箇所

  1. Service Objectを直接インスタンス化しないこと

Service Objectをインスタンス化しても、単にcallメソッドを実行する以外にあまり使いみちがありません。

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

  2. Service Objectのコンストラクタをシンプルに保つ
    一般に、実装するほとんどのクラスではコンストラクタをシンプルにしておくのがよい考えです。Serviceの主な呼び出し方法はクラスメソッドを呼び出すことですが、コンストラクタの責務を「引数をServiceのインスタンス変数に保存すること」に限定する方がずっとメリットが多くなります。

sample.rb
class DeleteUser
  def initialize(user_id:)
    @user_id = user_id
  end
  def call
    #…
  end
  private
  attr_reader :user_id
  def user
    @user ||= User.find(user_id)
  end
end

コンストラクタではなくcallメソッドに集中できるようになりますし、コンストラクタに何を置いてもよく、何を置いてはいけないかという線引きも明確になります。開発者が日々決定しなければならないことはたくさんあるので、こういう点を標準化して決めごとをひとつ減らしましょう。

7.結果はステートリーダー経由で返す

Service Objectはcallメソッドの結果は、たとえば次のように返すことが可能です。実行が成功した場合はtrue、失敗した場合はfalseを返す、という具合です。

しかし、callメソッドがService Object自身を返すようにすれば、もっと柔軟になります。この方法にすれば、Service Objectインスタンスのステートを読み出せるようになります

8.callメソッドの可読性を下げないよう注意する

callメソッドはService Objectの中心となるメソッドです。Service Objectでは、callメソッドをできるだけ読みやすく保つことをおすすめします。callメソッドには関連する手順だけを記述し、それ以外のロジックは最小限に抑えるようにします。andやorなどを使って、特定の手順のフローを制御してもよいでしょう。

class DeleteUser
  #…
  def call
    delete_user_comments
    delete_user and
      send_user_deletion_notification
  end
private
  #…
end