Rails のサービスクラスでのマイルールとちょっとしたコツ

# 動機

[Railsにおけるサービスクラスのオリジナルルール](http://qiita.com/chrischris0801/items/58a12d17a440b842db02) という記事をたまたま見つけ、「自分ならこう書くかな」と感じたことがいくつかあったので、記事にしてみました。
なお :point_up_2: の参考記事の中でさらに参考にされている [肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)](http://techracho.bpsinc.jp/hachi8833/2013_11_19/14738) という記事のコードを、この記事でも説明のために用いようと思います。そこで、以下は 2 つの記事をこのように呼称します。

* **参考記事 1**: [Railsにおけるサービスクラスのオリジナルルール](http://qiita.com/chrischris0801/items/58a12d17a440b842db02)
* **参考記事 2**: [肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)](http://techracho.bpsinc.jp/hachi8833/2013_11_19/14738)
   * さらに翻訳元の記事: [7 Patterns to Refactor Fat ActiveRecord Models](http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/)

# マイルール

## ルール 1. クラス名は「動詞 (+ 目的語)」にする

参考記事 1 のものとほぼ同じルールです。末尾に Service と付けないことのみ異なります (ただの好み) 。
参考記事 2 では `UserAuthenticator#authenticate` というメソッドが提供されています。これ以外でもよく `UsersExcelImporter#import`, `UploadedFileConverter#convert` のように、`-or`, `-er` という接尾語を持った名詞のクラス名 + 動詞のメソッド名という命名パターンをしばしば見かけます。これだと冗長な命名に感じるし、クラスの命名に迷う場合もあるので、僕は参考記事 1 のルールが非常に好みです。

## ルール 2. 外部に公開するメソッドは call という名前のクラスメソッドのみ

これがキモです :cupid: 

ルール 1 でクラス名は動詞を表すものになっているので、このクラスの責務は自身が持つ名前の処理を実行することのみであるのが理想的だと考えています。そこで「実行する」や「呼び出す」といった意味の public メソッドを 1 つだけ用意するのが理想です。
僕は今まで様々なメソッド名、例えば `do` や `execute`, `perform` などを採用しましたが、最近は `call` を使っています。なぜなら `Proc#call` とインターフェイスを合わせることができるからです。一定の手続きを実行するという意味で、サービスクラスの実行は Proc オブジェクトの実行と似ていると思います。

このルールに従って、参考記事 2 に記載されている :point_down: のコードを

```ruby
class UserAuthenticator
  def initialize(user)
    @user = user
  end

  def authenticate(unencrypted_password)
    return false unless @user

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

:point_down: のように書き換えてみます。なお、参考記事 1 の「引数は出来る限り new で渡してインスタンス化する」というルールに従って実装しています。

```ruby
class AuthenticateUser
  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
```

```ruby
# 呼び出し元の例
AuthenticateUser.new(User.find(1), 'password').call #=> true/false
```

ここで僕が感じるのが、1 度だけ `call` するためだけにわざわざ `new` でインスタンス化するのも冗長だなということです。そこで `AuthenticateUser#call` のクラスメソッド版である `AuthenticateUser.call` を追加します。

```ruby
class AuthenticateUser
  def self.call(user, unencrypted_password)
    new(user, unencrypted_password).call
  end

  ### 略 ###
end
```
```ruby
# 呼び出し元の例
AuthenticateUser.call(User.find(1), 'password') #=> true/false
```

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

これで、ほぼ理想のコードになりました :blush: 

ただ、このままではインスタンスメソッド経由でもクラスメソッド経由でも `call` を呼び出すことが可能です。これでは一貫性がなく呼び出し方を統一できないので、クラスメソッド経由での呼び出しに限定します。

```ruby
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
```

`AuthenticateUser#call` を private 化することで外部からの呼び出しを禁止します。また `private_class_method :new` を記述することで、インスタンス化も禁止します。

以上でマイルールに従った実装は完了です。しかし場合によっては、サービスクラスをさらに使いやすくするためにとあるメソッドを追加します。これはコツとして説明したいと思います。

# コツ

## to_proc というクラスメソッドを実装する

`call` の引数が一つの場合、 :point_down: のように `to_proc` というクラスメソッドを実装することで

```ruby
class AuthenticateUser
  private_class_method :new
  
  def self.call(user)
    new(user).send(:call)
  end

  def self.to_proc
    method(:call).to_proc
  end

  private

  def initialize(user)
    @user = user
  end

  def call
    return false unless @user

    if BCrypt::Password.new(@user.password_digest == 'master_password'.freeze
      @user
    else
      false
    end
  end
end
```

`Enumerable#each` などのメソッドの引数に Proc オブジェクトとして渡すことが可能になります。

```ruby
# AuthenticateUser.to_proc の例
users.map(&AuthenticateUser) #=> [false, false, #<User id: ...>, false, #<User id: ...>]
users.any?(&AuthenticateUser) #=> true

# 比較: Symbol#to_proc の例
(1..3).map(&:to_s) #=> => ["1", "2", "3"]
```

地味に便利です :innocent: 

# おまけ

マイルールを気軽に適用しやすくするための Module を実装しました :wink:

```rb
# サービスクラス用のインターフェイスを提供するモジュール。
# 使い方は以下のとおりです。
#
# (1) サービスクラスに Procedural を include する。
# (2) initialize を実装する。
# (3) private なインスタンスメソッド call を実装する。
#
# そうすると call というクラスメソッドのみを外部に公開したサービスクラスを作成できます。
# なお、クラスメソッド call の引数は initialize の引数と同じになります。
#
module Procedural
  extend ActiveSupport::Concern

  included do
    private_class_method :new
  end

  class_methods do
    def call(*args)
      instance = new(*args)
      yield(instance) if block_given?
      instance.send(:call)
    end

    def to_proc
      method(:call).to_proc
    end
  end

  private

  def initialize(*args)
    return if args.empty?

    raise(NotImplementedError, 'You must implement #{self.class}##{__method__}')
  end

  def call
    raise(NotImplementedError, 'You must implement #{self.class}##{__method__}')
  end
end
```

:point_down: 使用例

```rb
class AuthenticateUser
  include Procedural

  def initialize(user)
    @user = user
  end

  private
  
  def call
    return false unless @user

    if BCrypt::Password.new(@user.password_digest == 'master_password'.freeze
      @user
    else
      false
    end
  end
end
```