Railsでサービスクラスとフォームオブジェクトを使ってサッと良い感じにする


# サッと解決したいのでGemを使うよ
Rails界隈では定期的にサービスクラスとフォームオブジェクトの話題がきますよね。すぐモデルが肥大化しますからね。

ただ、そうした話題はウォッチしつつも、自分で全部設計するのはちょっとハードルが高いなと思っている方も多いのではないでしょうか。
特にサービスクラスを実装する記事見てると、めっちゃ色々考えてる感あるじゃないですか。大変そう。

そこで[Trailblazer](http://trailblazer.to/)ですよ！！！

Trailblazerは、Railsなどの上にサービスクラスなどのアーキテクチャを作るGemです。
昔流行った[7 Patterns to Refactor Fat ActiveRecord Models](http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/)（邦訳：[肥大化したActiveRecordモデルをリファクタリングする7つの方法](https://techracho.bpsinc.jp/hachi8833/2013_11_19/14738)）みたいなのを、考えずにササッと作れます。

Railsと同じように、賢い人が作り、進化し、メンテナンスされるアーキテクチャに沿って生きたい気持ちです。

# いやウチは新プロジェクトじゃないから…
新しいアーキテクチャを採用するのはプロジェクト開始時でなければ辛いと思われるかもしれませんが、そんなことはありません。
Trailblazerはとても現実的なGemなので、既存のコードと平行で使えます。つまり、少しずつ移行することができるのです。 [^1]

[^1]: 実際、弊社では半分ぐらいがTrailblazerを使って書かれてますが、半分はそのままのRailsになってます。

そこで、今回は既存のアプリケーションの一部を書き換えていく形で紹介したいと思います。
題材は有名な[Ruby on Rails チュートリアル](https://railstutorial.jp/)の[コード](https://bitbucket.org/railstutorial/sample_app_4th_ed)です。

本来はもっと難しいコードの方が有り難みが感じられるのですが、難しいコードを読みながら新しいことを覚えるの辛いと思うので、意味のないような簡単なコードで説明します。

# やってみよう！
## まずは動作確認
元のコードを動かすところから始めましょう。
Ruby on Rails チュートリアルの[リポジトリの説明を見ながら](https://bitbucket.org/railstutorial/sample_app_4th_ed)元のアプリが正常に動くか確認します。

```
$ git clone https://bitbucket.org/railstutorial/sample_app_4th_ed.git sample_app
$ cd sample_app
$ bundle install --without production
$ rails db:migrate
$ rails server
```
アプリが立ち上がったら、 http://localhost:3000/signup にアクセスしてみましょう。
![スクリーンショット 2017-07-28 10.39.33.png](https://qiita-image-store.s3.amazonaws.com/0/42541/17bcd6a4-6877-11c4-aba8-ebf73c72d072.png)
動いてますね。

## Trailblazerを使う準備をする
早速Trailblazerを使う準備をしましょう。GemfileにTrailblazerを追加します。

```
gem "trailblazer"
gem "trailblazer-rails"
gem "trailblazer-cells"
```

Gemfileに追加したGemをインストールします。

```
$ bundle install
```
Gemはインストールできましたので、次はディレクトリの作成です。
Trailblazerでは、`app`の下に`concepts`というディレクトリを作成して、そこに使用するコードを入れていきます。
今回はUserというモデル周りを弄るので、次のようなディレクトリ構造にしてください。
<img width="391" alt="スクリーンショット 2017-07-28 10.44.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/42541/67674bc9-8127-c7e4-afe7-3c295325cf02.png">

謎の`contract`と`operation`というディレクトリもありますが、ひとまず作っちゃいましょう。

## 今回書き換える場所
http://localhost:3000/signup での処理を対象とします。
フォームを表示するのは`UsersController#new`で、`UsersController#create`にPOSTするとデータが作られます。
`new`アクションで表示して`create`アクションでデータを作る流れを、Trailblazerを使って書き換えていきます。

## フォームオブジェクトを作る
それでは、フォームオブジェクトから追加していきます。

フォームオブジェクトでは、ユーザからの入力を受け付けてバリデーションを行います。[^2]
フォームオブジェクトを使用する場合、**モデルではなくフォームオブジェクトでバリデーションを行います**。
[^2]: フォームオブジェクトでは複数のモデルをまとめて処理するなど、もっと高度なことができるのですが、例が簡単すぎて他に書くことがありません :cry: ）
なので、次のようにUserモデル（`app/models/user.rb`）からバリデーションを消しまして…

![スクリーンショット 2017-07-28 10.57.39.png](https://qiita-image-store.s3.amazonaws.com/0/42541/25368281-9b89-96b0-b915-1678b366bd2a.png)

`app/concepts/user/contract/create.rb`にファイルを作成し、以下のようなフォームオブジェクトを書きます。

```app/concepts/user/contract/create.rb
require "reform/form/validation/unique_validator"

module User::Contract
  class Create < Reform::Form
    property :name, validates: { presence: true, length: { maximum: 50 } }
    VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
    property :email, validates: {
        presence: true, length: { maximum: 255 },
        format: { with: VALID_EMAIL_REGEX },
        unique: { case_sensitive: false }
    }
    property :password, validates: { presence: true, length: { minimum: 6 }, allow_nil: true, confirmation: true }
    property :password_confirmation, validates: { presence: true, length: { minimum: 6 }, allow_nil: true }
  end
end
```
いきなり大量のコードが出現したのでウッ…となりますね。少しずつ見ていきましょう。

まず、Trailblazerでは、フォームオブジェクトは`Reform::Form`というクラスを継承して使います。

```ruby
module User::Contract
  class Create < Reform::Form
    # 処理を書く
  end
end
```

クラスの中身には、以下のようなコードが書かれていますね。

```ruby
property :hoge, validates: {...}
```
このコードで、フォームオブジェクトに`hoge`というgetterとsetterを作成して、そこに入った値にバリデーションを行います。
バリデーションの中身は元のモデルからほぼコピペしてるだけなので、詳細は見なくて大丈夫です。

`email`の`unique`バリデーションだけ少し特殊で、元は`uniqueness: {...}`と書いてましたが、`unique: {...}`になってます。
`uniqueness`はTrailblazerでは動きません。理由を話すと長くなってしまうので、とりあえずunique使ってれば基本問題ないです。詳しくは[ドキュメント](http://trailblazer.to/gems/reform/validation.html#uniqueness-validation)を参照してください。
`unique`を使えるようにするために、一行目の`require`も必要になります。

## `new`アクションのサービスクラスを追加する
フォームオブジェクトはできたので、実際に使うためにサービスクラスも作っていきましょう。
Trailblazerでは、`Trailblazer::Operation`を継承するクラスがサービスクラスになります。

まず、`UsersController#new`のアクションで使用するサービスクラスから書いていきましょう。

`app/concepts/user/operation/create.rb`を作成し、次のようにします。

```app/concepts/user/operation/create.rb
class User::Create < Trailblazer::Operation
  class Present < Trailblazer::Operation
    step Model(User, :new) # User.newする
    step Contract::Build(constant: User::Contract::Create) # フォームオブジェクトをビルドする
  end
end
```

ファイル名が`new.rb`ではないことに注意してください。今後`create`アクションのコードもここに追加するので、`create`するためのサービスクラスとして`create.rb`という名前で追加します。

内容を見ていきましょう。

```ruby
class Present < Trailblazer::Operation
  step Model(User, :new) # User.newする
  step Contract::Build(constant: User::Contract::Create) # フォームオブジェクトをビルドする
end
```

`step`が並んでますが、その名の通り、上から順番に`step`が実行されていきます。

`Model(User, :new)`はそのまんま`User.new`で、モデルのオブジェクトを新しく作成します。

`Contract::Build`では、先ほど作ったフォームオブジェクト（`Reform`）のクラスをビルドします。
`Operation`は`Controller`でフォームオブジェクトを利用できるようにする役割も持っているので、ここでビルドしないと`Reform`のクラスはオブジェクトとして利用することができません。

## `new`アクションからの呼び出し
サービスクラスが追加できたので、Controllerから呼び出してみましょう。

```app/controllers/users_controller.rb
def new
  run User::Create::Present
end
```
これで呼び出せます。簡単ですね。
元々あった、`User.new`するコードはサービスクラスに移譲されているので消えてます。
![スクリーンショット 2017-07-28 14.49.23.png](https://qiita-image-store.s3.amazonaws.com/0/42541/770fe6df-be1a-2413-b30a-e203e8d71f2d.png)

## Viewからフォームオブジェクトを使う
サービスクラスをControllerから実行できたので、サービスクラスの中で実行しているフォームオブジェクトのビルドはできてます。
あとは、ビルドしたフォームオブジェクトをViewで使うだけです。

対象のViewは`app/views/users/new.html.erb`ですね。
フォームオブジェクトの使用は超簡単で、思考停止して次のように書き換えます。
![スクリーンショット 2017-07-28 11.31.48.png](https://qiita-image-store.s3.amazonaws.com/0/42541/a383f8d2-3f94-e8ca-813e-e3814fa77f19.png)

唐突に`@form`という変数が使えるようになってますが、Controllerで`run`メソッドを使用すると、ビルドされた`Reform`のフォームオブジェクトが`@form`に自動的に代入されます。便利！

## `new`アクションの挙動を確認する

一旦ここまでで http://localhost:3000/signup を表示してみましょう。
Gemなどを追加した関係で名前空間を再読み込みしたいので、一旦Ctrl+Cで起動してるサーバを落とし、再度立ち上げます。

```
$ rails server
```

特に前回までと変わりない画面が表示されているはずです。
（`create`アクションを書き換えてないので、`submit`しても動きません）

## `create`アクションのサービスクラスを書く
本命である`UsersController#create`アクションのサービスクラスを書いていきましょう。

元のControllerのコードはこんな感じです。

```app/controllers/users_controller.rb
def create
  @user = User.new(user_params)
  if @user.save
    @user.send_activation_email
    flash[:info] = "Please check your email to activate your account."
    redirect_to root_url
  else
    render 'new'
  end
end
```

よくある感じですね。

それでは、この処理を移譲するサービスクラスを書きましょう。
後で詳しく見ていくので、`app/concepts/user/operation/create.rb`に一旦次のように追加してみて下さい。

```app/concepts/user/operation/create.rb
class User::Create < Trailblazer::Operation
  class Present < Trailblazer::Operation
    step Model(User, :new) # User.newする
    step Contract::Build(constant: User::Contract::Create) # フォームオブジェクトをビルドする
  end

  # --- ここから下を追加 ---
  step Nested(Present)
  step Contract::Validate(key: :user)
  step Contract::Persist()
  step :send_activation_email
  
  def send_activation_email(options)
    user = options["model"]
    UserMailer.account_activation(user).deliver_now
  end
end
```
眺めてみていかがでしょうか…？
意味が分からない方がほとんどだと思うので、順番に解説していきます。

### Nested

まず、`step Nested(Present)`ですが、これは`Present`クラスの`step`を呼び出して実行しますよーという意味です。
以下のようなコードがありますよね。

```ruby
class Present < Trailblazer::Operation
  step Model(User, :new)
  step Contract::Build(constant: User::Contract::Create)
end

step Nested(Present)
```

これは次のコードと同じです。

```ruby
class Present < Trailblazer::Operation
  step Model(User, :new)
  step Contract::Build(constant: User::Contract::Create)
end

step Model(User, :new)
step Contract::Build(constant: User::Contract::Create)
```

`Present`はクラス内クラスとして存在しているだけなので、外側のクラスから使いたい場合は明示的に呼び出す必要があるということです。


ところで、このコードでは、`UsersController#new`でフォームを表示する時と同じように、`User.new`を行い、その後にフォームオブジェクトを作成しています。
フォームオブジェクトは`UsersController#new`の表示にも必要ですが、データをPOSTされた時に受ける（そしてバリデーションエラーがあればそれをフォームに表示する）必要があるので、ここでも必要になります。

### validateとpersist
```ruby
step Contract::Validate(key: :user)
step Contract::Persist()
```

POSTで送信されてきたデータに対して、フォームオブジェクトで書いたバリデーションを行うのが`Contract::Validate`です。
引数の`key`は、`params`の中のどの`key`がバリデーションの対象になるかを指定します。
バリデーションを行うタイミングで、`params`のデータとフォームオブジェクトをマッピングしているので、このような記述になります。

`Persist`は、フォームオブジェクトが持っている（バリデーションが通った）データを`User.new`したモデルに送り`、save`します。

### バリデーションが通らないとどうなるの？
ここで不思議なのは、下記のような通常のRailsにあるはずのバリデーションの結果による`if`がないことですね。

```ruby
def create
  @user = User.new(user_params)
  if @user.save
    # 成功した場合の処理
  else
    # 失敗した場合の処理
  end
end
```

Trailblazerの`Operation`では、`step`は上から順番に実行されるので分岐どこいったという感じがしますが、実は**stepの実行が失敗すると、それ以上stepが進まなくなる**のです。
この話をすると長くなってしまうので詳しくは書きませんが、`Operation`では成功のラインと失敗のラインがあり、この2つで遷移を管理します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42541/4d672dd7-d5f4-b9c3-70d9-eb9d72bd349f.png)
（ http://trailblazer.to/gems/operation/2.0/index.html#flow-control から引用）

この図だと、成功している限りは右の`step`を順番にこなし、失敗すると左のラインに移動して処理を終えます。

この仕組みが実に秀逸で、例えば保存処理をして良いユーザであるかチェックするとか、そういう[チェック処理](http://trailblazer.to/gems/operation/2.0/policy.html)もこの2つのラインで管理できます。
誰が書いてもそれなりに見やすく書ける感じが好きです。


### メール送信
サービスクラスでメール送信している部分について見ていきます。
元の`create`アクションでは、`@user.save`が通った場合、メールを送ることになってました。

```ruby
def create
  @user = User.new(user_params)
  if @user.save
    @user.send_activation_email # ←これ
    # ...
  else
    # ...
  end
end
```

この`send_activation_email`はモデルにありますが、他から呼び出されていないようなので、実装も呼び出しも以下のように`Operation`に書いています。

```ruby
step :send_activation_email

def send_activation_email(options)
  user = options["model"]
  UserMailer.account_activation(user).deliver_now
end
```

`Operation`の`step`で呼び出せるメソッドは、必ず引数を取る必要があります。この中に`model`があるので、ここから`step Contract::Persist()`で保存された後のUserモデルを取得できます。

## createアクションを修正 する
さて、`Operation`は完成したので`UsersController#create`から呼び出したいと思います。次のように書き換えましょう。

```app/controllers/users_controller.rb
def create
  run User::Create do |_|
    flash[:info] = "Please check your email to activate your account."
    redirect_to root_url and return
  end
  render 'new'
end
```
`new`アクションの時とは違い、`run`メソッドがブロックを持っていますね。
`run`メソッドのブロックの中は`step`が全て成功した場合に実行されます。この性質を利用して、次のように成功した場合と失敗した場合を分けます。

```ruby
run User::Create do |_|
  # 成功した場合の処理
  return # returnするので、成功したらここでメソッドを抜ける
end
# 失敗した時の処理
```

全ての処理が`Operation`の中の実装で書ければ良いのですが、現実的には`Controller`で実行しないと動かない処理はありますので、このようにブロックの内外に記述します。
`Operation`に入れないものとしては、`redirect_to`などですね。`redirect_to`を使う時はControllerのオブジェクトが`self`になることを期待していますが、`Operation`で実行すると`self`が変わってしまいます。[^3]
[^3]: `run`は引数に自由に渡せるので、Controllerのオブジェクトを渡したりすれば`Operation`の中で書ける気がしなくもないですが、そういった書き方をしてる人は見たことないです。

## 不要なコードのを削除
最後に、不要になったコードを消しましょう。

まず、Userモデル(`app/models/user.rb`)でメールを送っていた部分は消しましょう。
![スクリーンショット 2017-07-28 14.48.49.png](https://qiita-image-store.s3.amazonaws.com/0/42541/19baa63e-9f67-5796-2c07-b0e1d6910c09.png)

また、ControllerのStrongParametersの処理は、フォームオブジェクトで生やしているプロパティしかそもそもモデルに入らないので、不要になります。
![スクリーンショット 2017-07-28 14.48.30.png](https://qiita-image-store.s3.amazonaws.com/0/42541/fd9ffc38-b32e-72e8-0cfe-2505bf5b207e.png)
…ただ、今回は別のアクション（`UsersController#update`）でも使用してるので、上のようにメソッドごと消しちゃうとアレな感じになります。ご注意ください。

## 動かす
http://localhost:3000/signup を試してみましょう！
（メールの設定がテストモードになってるので、メールは送られません）

今回行なった修正は[githubに置いときました](https://github.com/kazekyo/rails_tutorial_trailblazer/compare/b7db784f704e127186b16c14e320478818c5dc9d...master)ので、動かねーという方はそちらも参照して頂ければと思います。
ちなみにテストまでは面倒見てませんので動かないかもしれません。

# 終わりに
全てのクラスが役割ごとにスッキリまとまって良い感じになりましたね。
あまり考えずにサッとサービスクラスとフォームオブジェクト作れるの最高すぎます。

Trailblazerは他にも様々なアーキテクチャを使えますので、そちらもまた紹介できればと思います！