TrailblazerのReformを使ってRailsのフォームとモデルを分離する
これはFOLIO Advent Calendar19日目の記事です。昨日は@asuyakonoの「デザイナーの僕がいかにしてビデオゲームのUIから「インタラクション・デザイン」を学ぶのか」でした。ゲームをやらない私にとってもめっちゃ面白くて分かりやすい記事だったし、ブクマ数もすごいことになっていました。さ、さすが・・・
さて、このアドベントカレンダーは2回目の登場です。前回は割とエモい記事(証券会社のエンジニア・デザイナーが社外でも最大限活躍するためにFOLIOで取り組んでいること)だったので今回は技術的な話を・・・ということでRuby on Railsについて書きます。
FOLIOでは、サービスとして外部に公開しているフォリオのみならず、社内で証券業務を行うために使うシステムを自前で開発しています。Scala製のマイクロサービスたちがバックエンドに構えていて、そのAPIを用いて機能提供するクライアントとして稼働しているのがRailsで作られたアプリです。最近は専らそのシステムを担当しており、入社前はほとんど書いたことのなかったRubyとも仲良くやっています(たぶん)。
今回は、FOLIOでも一部導入しているTrailblazerというgem群から、Reformというgemについて調べてみました。
業務において
- バリデーション、エラーハンドリング、エラーメッセージの指定をどのレイヤーでやるのがよいかな〜って悩んだ経験があること
- モデルクラスにおいて、入力値(フォーム)とAPIから受け取る値の区別が曖昧で管理がしづらくなっているのを何度か見かけたこと
がきっかけです。
Trailblazerの概要
TrailblazerはRubyのフレームワークに抽象レイヤーを提供するgemの集まりで、モデルやコントローラーからビジネスロジックを排除することを目指しています。純粋なRails wayでの開発にありがちなクラスの肥大化を防ぎ、メンテナビリティを上げるのに一役買ってくれそうなアーキテクチャです。
READMEも公式ドキュメントも丁寧だし、無料で手に入る情報だけで大事なことが詳しく分かります。
Trailblazerの作者が思想・使い方などについて書いた本もあって、最近気になる箇所だけつまみ食いのように読んでいます。著者も「興味のあるところから読んでね!」と言っているし、途中の章からでも理解できるような構成となっていて良いです。
余談ですが、日本語の情報が少ない印象を受けますね。
Trailblazerを構成するgemたち
先程gemの集まりと述べましたが、どういったものがあるのか簡単に見てみます。
- Operation: サービスオブジェクト。ビジネスロジックはこれに持たせ、モデルやコントローラーからは取り除く。
- Cells: UIのパーツを表現するオブジェクト。テンプレートをレンダリングするオブジェクトとも言える。
- Reform: モデルにバリデーションのためのフォームオブジェクトを提供する。
- Representable: オブジェクトとJSONやXMLなどドキュメント間の変換を行う(レンダリング・パース)。
その他、まだバージョン1.0の出ていないRoar、Formula、Disposableなんかもありますが割愛します。
Operationの使い方をざっと
Trailblazerの大きな特徴でありメインともいえる(個人の感想)gemです。Reformの解説でもちらっと登場するので簡単に紹介します。
Operation
はいわゆるサービスクラスにあたり、ビジネスロジックをカプセル化するのに有効です。サンプルコードを先に出します。
class Hoge::Create < Trailblazer::Operation step :process_hoge! step :process_fuga! failure :log_error! success :process_piyo! def process_hoge! # 処理。異常値を返したらlog_errorへ移る。 end def process_fuga! # 処理。異常値を返したらlog_errorへ移る。 end def log_error! # エラーハンドリングの処理。前段の処理でトラックが左側に移ったら実行される。 end def process_piyo! # 処理。前段の処理でトラックが左側に移っていなければ実行される。 end end
ビジネスロジックを処理のまとまりごとに分け、それぞれメソッド(上の例で言うとprocess_hoge!
とかlog_error!
とか)として定義します。
※公式(http://trailblazer.to/gems/operation/2.0/index.html#flow-control)より
メソッド化した各処理は左右2トラックのフロー(パイプと呼ぶらしい)に分かれ、正常系では右側のトラックを上から実行していき、異常系に遷移したら左側のトラックへ移ります。定義する際に step
, success
, failure
といった3つのAPIを使い分けることでどちらのトラックに配置するかを指定し、意図した順序で呼び出すことが可能です。
- step: 処理を右側のトラックに置く。異常値を返す処理が実行された時点で、パイプが左側のトラックへと移る。
- success: 処理を右側のトラックに置く。処理の返却値は考慮しない。
- failure: 処理を左側のトラックに置く。エラーハンドリングが目的。処理の返却値は考慮しない。
ネストが減ってフラットに処理が並ぶので流れを追いやすいし、正常系・異常系に分けて考えられるので分かりよいです。
Reformの使い方をじっくり
さて、この記事の本題へ。
特徴
Reformは先に書いたとおりバリデーション用のフォームオブジェクトを提供します。
- フレームワークに依存せず、Rails, Sinatra, Hanamiなど多くのプロジェクトで使える
- データベースは意識しないので、どんなORMと一緒でも使える
- データベースアクセスはせず、モデルのプロパティの書き込み・読み込みのみ行う
- モデルをフォームオブジェクトにマッピングすることが出来る
といった特徴がありますが、このあとの使い方を見てしまった方が理解しやすいはずです。
使い方
なお、ここからはビールが当たるキャンペーンの応募フォームを想定して話を進めます。画面の構成要素はこんな感じ。
- 名前入力用テキストフィールド
- 住所入力用テキストフィールド
- あなたは20歳以上ですか?確認用チェックボックス
※イメージ
定義
ではコードと一緒に使い方を見ていきます。
Reform::Form
を用いてクラスを定義するところから。フィールド定義はproperty
, collection
というキーワードで行い、バリデーションもこのクラスで指定します。
class ApplyBeerForm < Reform::Form # フィールド定義 property :name property :address property :over_nineteen #ActiveModel::Validationを用いたバリデーション validates :name, presence: true, length: { maximum: 20 } validates :address, presence: true, length: { maximum: 100 } validates :over_nineteen, acceptance: true end
バリデーションの定義はActiveModelの他にdry-validationも用いることができるようです。というかActiveModelは非推奨となっている模様ですね・・・(詳しくは: installation)。
なお、オペレーションの中ならcontract
を定義してやるとReform::Form
インスタンスが生成されます(詳しくは: validation)。
contract do property :name property :address property :over_nineteen validates :name, presence: true, length: { maximum: 20 } validates :address, presence: true, length: { maximum: 100 } validates :over_nineteen, acceptance: true end
Reform
はフォームとモデルを分離する仕組みを取り入れている(詳しくは後述)ので、両者を別物として扱うのが比較的容易です。そのため、例えば「フォームには存在するがモデルには不要な値」なんかも自然に定義出来て便利です。この例だと、20歳以上かのチェックがそれに該当しますね。
インスタンス生成
コントローラーかオペレーションでインスタンスを作ります。
まず、Applicant
(応募者)インスタンスを新しく作ってフォームインスタンスに渡すパターン。
@form = ApplyBeerForm.new(Applicant.new) #モデルのインスタンスを生成して渡す
続いて、作成済みApplicant
を渡すパターン。渡したインスタンスは編集対象となります。このとき、Reformが既存のApplicant
インスタンスに1度だけアクセスし、値を読み込んでくれます。
@form = ApplyBeerForm.new(Applicant.find(1)) #findしたモデルのインスタンスを渡す
バリデーション
#form_for
とかを使ってフォームインスタンスをレンダリングしたのち、validate
メソッドを呼び出します。バリデーションの対象はフォームオブジェクトのフィールドとして定義した値のみで、それ以外が渡された場合は無視されます。
if @form.validate(params[:applicant]) @form.save #後述 else #エラーハンドリングする end
バリデーションの結果、問題なかった場合はフォームに値が反映されます。一方、バリデーションエラーとなったら、errors
がエラーメッセージを返します。
重要なのは、値が有効で無事フォームの値が更新されたとしても、モデル側(ApplicantModel
)にはまだ変更が入らないことです。
モデルに反映・永続化
インプットをモデルにも反映するには、#save
か#sync
を呼び出します。これにより、Reformがモデルのsetterを使って値を書き換えにかかります。
@form.save #モデルに反映し、永続化する
@form.sync #モデルへの反映のみ行う
バリデーションが終わり、明示的にメソッドをコールするまではモデルの更新が行われないというわけです。フォームとモデルが、意味的にも実際の挙動としても綺麗に分かれてくれていることがよく分かりましたね。
順番をおさらい
Reformの使い方を追ってきましたが、流れをまとめるとこんな感じです。
- 定義
- インスタンス生成
- バリデーション
- モデルに反映・永続化
この手順を踏むことについて、Trailblazerの作者は「やることが多くて面倒だと思うかもしれないけどシンプルなんだよ」みたいな予防線を張ってますが、自分は割とすんなり理解・納得できました。
ちなみに、序盤でフォームの扱いに悩んだことがあると書きましたが、最近フォームを扱う画面を作る際は
- フォーム用モデル(
Reform::Form
ではなく、form以外のモデルと同様に定義している)をつくって、そのクラス内でバリデーションする - オペレーションにてバリデーション結果をチェック(し、エラーハンドリング)する
- バリデーションにひっかからなければ右側のトラックで次のステップへ進む
- ひっかかれば左側のトラックへ移り、エラーメッセージをコントローラーへ返す
こんな感じにしていて、これはこれで割といい感じにまとまりつつある気はしています。ただ、Trailblazerだけで教科書通りやるとこうなるんだなーという学びは面白かったし、フォームもモデルも複雑な画面で使うとどうなるのかしらという思いがあるので、業務でもちょっと試してみたいです。
Trailblazer、FOLIOでは主にAPI呼び出しに関連する部分から徐々に取り入れています。そういった部分的な導入により既存コードのリファクタリングをしていく方針を作者も良しとしており、本でも推奨されています。それもあってRails wayとの共存がしやすい作りになっており、導入しやすいのもメリットかなぁと思います。
機能はまだまだたくさんあるし、また何か困ったことがあったらちょっとずつ試してみたいなーと思います。自分が担当しているシステムはドメイン知識もたくさん要求されるし複雑な部分もあるので、せめてアーキテクチャは工夫してメンテしやすさを追求していきたいものです💪