サッと解決したいのでGemを使うよ
Rails界隈では定期的にサービスクラスとフォームオブジェクトの話題がきますよね。すぐモデルが肥大化しますからね。
ただ、そうした話題はウォッチしつつも、自分で全部設計するのはちょっとハードルが高いなと思っている方も多いのではないでしょうか。
特にサービスクラスを実装する記事見てると、めっちゃ色々考えてる感あるじゃないですか。大変そう。
そこでTrailblazerですよ!!!
Trailblazerは、Railsなどの上にサービスクラスなどのアーキテクチャを作るGemです。
昔流行った7 Patterns to Refactor Fat ActiveRecord Models(邦訳:肥大化したActiveRecordモデルをリファクタリングする7つの方法)みたいなのを、考えずにササッと作れます。
Railsと同じように、賢い人が作り、進化し、メンテナンスされるアーキテクチャに沿って生きたい気持ちです。
いやウチは新プロジェクトじゃないから…
新しいアーキテクチャを採用するのはプロジェクト開始時でなければ辛いと思われるかもしれませんが、そんなことはありません。
Trailblazerはとても現実的なGemなので、既存のコードと平行で使えます。つまり、少しずつ移行することができるのです。 1
そこで、今回は既存のアプリケーションの一部を書き換えていく形で紹介したいと思います。
題材は有名なRuby on Rails チュートリアルのコードです。
本来はもっと難しいコードの方が有り難みが感じられるのですが、難しいコードを読みながら新しいことを覚えるの辛いと思うので、意味のないような簡単なコードで説明します。
やってみよう!
まずは動作確認
元のコードを動かすところから始めましょう。
Ruby on Rails チュートリアルのリポジトリの説明を見ながら元のアプリが正常に動くか確認します。
$ 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 にアクセスしてみましょう。
動いてますね。
Trailblazerを使う準備をする
早速Trailblazerを使う準備をしましょう。GemfileにTrailblazerを追加します。
gem "trailblazer"
gem "trailblazer-rails"
gem "trailblazer-cells"
Gemfileに追加したGemをインストールします。
$ bundle install
Gemはインストールできましたので、次はディレクトリの作成です。
Trailblazerでは、app
の下にconcepts
というディレクトリを作成して、そこに使用するコードを入れていきます。
今回はUserというモデル周りを弄るので、次のようなディレクトリ構造にしてください。
謎のcontract
とoperation
というディレクトリもありますが、ひとまず作っちゃいましょう。
今回書き換える場所
http://localhost:3000/signup での処理を対象とします。
フォームを表示するのはUsersController#new
で、UsersController#create
にPOSTするとデータが作られます。
new
アクションで表示してcreate
アクションでデータを作る流れを、Trailblazerを使って書き換えていきます。
フォームオブジェクトを作る
それでは、フォームオブジェクトから追加していきます。
フォームオブジェクトでは、ユーザからの入力を受け付けてバリデーションを行います。2
フォームオブジェクトを使用する場合、モデルではなくフォームオブジェクトでバリデーションを行います。
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
というクラスを継承して使います。
module User::Contract
class Create < Reform::Form
# 処理を書く
end
end
クラスの中身には、以下のようなコードが書かれていますね。
property :hoge, validates: {...}
このコードで、フォームオブジェクトにhoge
というgetterとsetterを作成して、そこに入った値にバリデーションを行います。
バリデーションの中身は元のモデルからほぼコピペしてるだけなので、詳細は見なくて大丈夫です。
email
のunique
バリデーションだけ少し特殊で、元はuniqueness: {...}
と書いてましたが、unique: {...}
になってます。
uniqueness
はTrailblazerでは動きません。理由を話すと長くなってしまうので、とりあえずunique使ってれば基本問題ないです。詳しくはドキュメントを参照してください。
unique
を使えるようにするために、一行目のrequire
も必要になります。
new
アクションのサービスクラスを追加する
フォームオブジェクトはできたので、実際に使うためにサービスクラスも作っていきましょう。
Trailblazerでは、Trailblazer::Operation
を継承するクラスがサービスクラスになります。
まず、UsersController#new
のアクションで使用するサービスクラスから書いていきましょう。
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
という名前で追加します。
内容を見ていきましょう。
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から呼び出してみましょう。
def new
run User::Create::Present
end
これで呼び出せます。簡単ですね。
元々あった、User.new
するコードはサービスクラスに移譲されているので消えてます。
Viewからフォームオブジェクトを使う
サービスクラスをControllerから実行できたので、サービスクラスの中で実行しているフォームオブジェクトのビルドはできてます。
あとは、ビルドしたフォームオブジェクトをViewで使うだけです。
対象のViewはapp/views/users/new.html.erb
ですね。
フォームオブジェクトの使用は超簡単で、思考停止して次のように書き換えます。
唐突に@form
という変数が使えるようになってますが、Controllerでrun
メソッドを使用すると、ビルドされたReform
のフォームオブジェクトが@form
に自動的に代入されます。便利!
new
アクションの挙動を確認する
一旦ここまでで http://localhost:3000/signup を表示してみましょう。
Gemなどを追加した関係で名前空間を再読み込みしたいので、一旦Ctrl+Cで起動してるサーバを落とし、再度立ち上げます。
$ rails server
特に前回までと変わりない画面が表示されているはずです。
(create
アクションを書き換えてないので、submit
しても動きません)
create
アクションのサービスクラスを書く
本命であるUsersController#create
アクションのサービスクラスを書いていきましょう。
元のControllerのコードはこんな感じです。
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
に一旦次のように追加してみて下さい。
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
を呼び出して実行しますよーという意味です。
以下のようなコードがありますよね。
class Present < Trailblazer::Operation
step Model(User, :new)
step Contract::Build(constant: User::Contract::Create)
end
step Nested(Present)
これは次のコードと同じです。
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
step Contract::Validate(key: :user)
step Contract::Persist()
POSTで送信されてきたデータに対して、フォームオブジェクトで書いたバリデーションを行うのがContract::Validate
です。
引数のkey
は、params
の中のどのkey
がバリデーションの対象になるかを指定します。
バリデーションを行うタイミングで、params
のデータとフォームオブジェクトをマッピングしているので、このような記述になります。
Persist
は、フォームオブジェクトが持っている(バリデーションが通った)データをUser.new
したモデルに送り、save
します。
バリデーションが通らないとどうなるの?
ここで不思議なのは、下記のような通常のRailsにあるはずのバリデーションの結果によるif
がないことですね。
def create
@user = User.new(user_params)
if @user.save
# 成功した場合の処理
else
# 失敗した場合の処理
end
end
TrailblazerのOperation
では、step
は上から順番に実行されるので分岐どこいったという感じがしますが、実はstepの実行が失敗すると、それ以上stepが進まなくなるのです。
この話をすると長くなってしまうので詳しくは書きませんが、Operation
では成功のラインと失敗のラインがあり、この2つで遷移を管理します。
( http://trailblazer.to/gems/operation/2.0/index.html#flow-control から引用)
この図だと、成功している限りは右のstep
を順番にこなし、失敗すると左のラインに移動して処理を終えます。
この仕組みが実に秀逸で、例えば保存処理をして良いユーザであるかチェックするとか、そういうチェック処理もこの2つのラインで管理できます。
誰が書いてもそれなりに見やすく書ける感じが好きです。
メール送信
サービスクラスでメール送信している部分について見ていきます。
元のcreate
アクションでは、@user.save
が通った場合、メールを送ることになってました。
def create
@user = User.new(user_params)
if @user.save
@user.send_activation_email # ←これ
# ...
else
# ...
end
end
このsend_activation_email
はモデルにありますが、他から呼び出されていないようなので、実装も呼び出しも以下のようにOperation
に書いています。
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
から呼び出したいと思います。次のように書き換えましょう。
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
が全て成功した場合に実行されます。この性質を利用して、次のように成功した場合と失敗した場合を分けます。
run User::Create do |_|
# 成功した場合の処理
return # returnするので、成功したらここでメソッドを抜ける
end
# 失敗した時の処理
全ての処理がOperation
の中の実装で書ければ良いのですが、現実的にはController
で実行しないと動かない処理はありますので、このようにブロックの内外に記述します。
Operation
に入れないものとしては、redirect_to
などですね。redirect_to
を使う時はControllerのオブジェクトがself
になることを期待していますが、Operation
で実行するとself
が変わってしまいます。3
不要なコードのを削除
最後に、不要になったコードを消しましょう。
まず、Userモデル(app/models/user.rb
)でメールを送っていた部分は消しましょう。
また、ControllerのStrongParametersの処理は、フォームオブジェクトで生やしているプロパティしかそもそもモデルに入らないので、不要になります。
…ただ、今回は別のアクション(UsersController#update
)でも使用してるので、上のようにメソッドごと消しちゃうとアレな感じになります。ご注意ください。
動かす
http://localhost:3000/signup を試してみましょう!
(メールの設定がテストモードになってるので、メールは送られません)
今回行なった修正はgithubに置いときましたので、動かねーという方はそちらも参照して頂ければと思います。
ちなみにテストまでは面倒見てませんので動かないかもしれません。
終わりに
全てのクラスが役割ごとにスッキリまとまって良い感じになりましたね。
あまり考えずにサッとサービスクラスとフォームオブジェクト作れるの最高すぎます。
Trailblazerは他にも様々なアーキテクチャを使えますので、そちらもまた紹介できればと思います!