153
139

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Trailblazerを使い、Railsのモデルの肥大化問題からサヨナラする

Posted at

はじめに

夏なのに家で冷房を入れることを禁止されているどうも鈴木です。まさに地獄です。

さて、コントーラーやモデルが肥大化する話はRailsを普通に使っていると誰しもが直面する話だと思われます。
例えばUserモデルのように、色々なところから参照されるモデルは地獄みたいなことになってしまいます。地獄は家だけにしてほしいですね。

世間ではサービス層だなんだと言われていますが、何となくapp/servicesなんてディレクトリを作り満足し、後で負の遺産とか言われるのはとても辛いです1。そもそもRails使ってるのはレールに乗るためなので、なるべく賢い人が作ったレールに乗って生きていくのが正義だと思っています2

ということで、私たちの会社では Trailblazer というgemを使い始めたので、ここで少し紹介します。
もちろん元気よく本番で動いてくれていますよ。

Trailblazerとは?

(出典:[http://trailblazer.to/](http://trailblazer.to/))

Trailblazerは、特定のアーキテクチャに沿って書くためのgemで、RailsやSinatraなどと一緒に利用できます。
この記事ではRailsと一緒に使う想定で説明を行います。

Trailblazerは設計が非常によく考えられており、学習コストはかかりますが結果的にRailsを普通に使うのに加えを以下のようなメリットが発生します。

  • リファクタリングしながら使い始められる
    • Trailbalzerの開発者曰く、ローマは一日にしてならずらしいです。(意訳)まずは使う、話はそれからだ
  • それぞれのコードの役割がはっきりとし、見やすく、探しやすく、さらにテストしやすくなる
  • 仕様変更に強い
  • ModelやControllerからビジネスロジックが無くなり3、Fat ModelやFat Controller問題が発生しない
  • チーム内でのコード水準のばらつきが抑えられる

既存のRailsコードと共存できたり少しずつ移行できるようになっているのは、導入の際にとても助かります。

Trailblazerの中身

複雑なコードほどTrailblazerを使う意味があるのですが、解説が難しくなるので、比較的シンプルなコードで解説したいと思います。

app/controllers/posts_controller.rb
before_action :authenticate_user!

def create
  post = Post.new(post_params)
  post.user = current_user

  if post.save
    post.notify_author!
    redirect_to action: :index
  else
    render :new
  end
end

def post_params
  params.require(:post).permit(:title, :body)
end
app/modes/post.rb
belongs_to :user

validates :user, presence: true
validates :title, presence: true, length: {maximum: 100}
validates :body, presence: true, length: {maximum: 2000}

def notify_author!
  # メール通知する
  # 例えば...
  # PostMailer.notice(user: self.user, post: self).deliver_now
end

これは見ての通りPostを保存するコードで、以下のような処理の集まりになっています。

  1. ユーザ認証
  2. StrongParametersでparamsを検証
  3. 新しいPostのオブジェクトを生成し、post変数に入れる
  4. postにユーザを紐付ける
  5. postをバリデーションする
  6. DBに保存する
  • 保存できればユーザに通知を飛ばし、indexに戻る
  • 保存できなければnewのテンプレートを表示する

Trailblazerでは、上記のような処理の1つ1つをStepと呼びます。
これらStepを管理するためのクラス(Operation)を別途作り、ControllerやModelとは分離してしまうのがTrailblazerです。下でもう少し詳しく説明していきます。

Operation

Trailblazerの主要な概念として、Operationがあります。
Operationは名前の通りで、上で列挙したStepをOperationする(上から実行してく)ための仕組みです。

先のコードをOperationにすると、以下のようになります。

app/concepts/post/operation/create.rb
class Post::Create < Trailblazer::Operation
  extend Contract::DSL

  contract do
    model "post"

    property :title, validates: { presence: true, length: { maximum: 100 } }
    property :body, validates: { presence: true, length: { maximum: 2000 } }
  end

  step Model(Post, :new)
  step :assign_current_user!
  step Contract::Build()
  step Contract::Validate()
  step Contract::Persist()
  step :notify_author!

  def assign_current_user!(options)
    post = options["model"]
    post.user = options["current_user"]
  end

  def notify_author!(options)
    # modelにはメソッドを生やさず、ここで通知を飛ばす
  end
end

雰囲気で読めなくもないですが、1つずつ解説していきます。

Contract(Reform)

まずは上半分、Contractです。

contract do
  model "post"

  property :title, validates: { presence: true, length: { maximum: 100 } }
  property :body, validates: { presence: true, length: { maximum: 2000 } }
end

Trailblazerでは、Strong Parametersとモデルのバリデーションを、Contractという場所で合わせて書きます。

ContractはReform::Formクラスを継承したものなのですが、このFormという名前が全てを表現しています。どの項目をどのように検証したいかは、モデル単位で考えるのではなくフォーム単位で考えるのがTrailblazerの考え方です。

例えば、以下の『規約に同意』のチェックのバリデーションのように、「モデルのacceptanceでやるのって正直気持ち悪いな...」と思いながら書いてしまっている箇所が数か所あるとおもいます。

# app/models/user.rb
class User
  validates :privacy_policy, acceptance: true, on: :create
end

自分も上のような、ある画面からの呼び出しの場合だけ有効になるようなコード をモデルに書いてしまっていたりしたのですが4、フォームの構造とモデルが密結合した状況になっていて、思えばモデルとは何だったのか感が出てしまっていました。なるべく画面都合のものはモデルに書きたくありません。
このような気持ち悪さを、Trailblazerはモデルではなくフォーム単位でバリデーションすることで解決します。

# app/models/user.rb
class User
end

# app/concepts/user/contract/create.rb
module User::Contract
  class Create < Reform::Form
    property :privacy_policy, virtual: true # virtualでカラムに存在しない値を送信できるようにしている
    validate :acceptance do
      errors.add(:privacy, "同意が必要です") if privacy_policy.to_i != 1
    end
  end
end

モデルからバリデーションが無くなり、作成時のフォームの要素定義と同時に、バリデーションも記述するようになりました。気持ちいいですね!

考え方が分かったところで、話を戻して実際のバリデーションしているコードを見ていきます。

property :title, validates: { presence: true, length: { maximum: 100 } }

外からの入力は、propertyで書いた項目しか値が入りません。つまり、これがStrong Parametersのpermitと同様の働きをしてくれています。property を書いた項目しか入力値とみなされないので、Mass Assignmentも安心です。
また、その項目に対してバリデーションをします。これは見れば分かると思います。

今回はContractを、量も少ないので(あとは説明を簡単にするため)Operationの中に書いてますが、別のファイルに書いてOperationの外に出すこともできます。大規模なフォームでもかなりシンプルに書くことができます。

Step

次に下半分のStepです。

step Model(Post, :new) # modelを生成する
step :assign_current_user! # modelにユーザを紐付ける
step Contract::Build() # contractを作る(上の方で定義しただけで使用する実体はないので、実体を作る)
step Contract::Validate() # contractを使ってバリデーションする
step Contract::Persist() # modelに保存する
step :notify_author! # 通知を飛ばす

Operationの真の力は、超簡潔なStepのフロー管理にあると言っても過言ではありません。

次の図のように、Operationは2つのレーンを持っていて、上から処理が実行されて行きます。右のレーンには成功の、左のレーンが失敗の処理が乗ります。

Flow Control
(出典:http://trailblazer.to/gems/operation/2.0/api.html#flow-control)

処理は右のレーンから始まります。右レーン上にある各Stepが成功する限り、右のレーンを走り続けます。Stepが失敗すると、失敗(左)のレーンに移動され、成功(右)のレーンに戻ってくることはありません。

なお、Step最後の:notify_author!は、元々Postモデルにあったメソッドです。このコンテキストからしか呼び出しがないメソッドなので、Stepとして移動してきました。
このように普段はモデルに生やすメソッドをOperationのStepとして書くことで、モデルの肥大化を防いでいきます。

Controllerからの呼び出し

さて、Operationが定義できたのでControllerから呼び出して見たいと思います。

def create
  run Post::Create, params[:post], current_user: current_user do
    # バリデーションに成功したら(全Stepが成功したら)こちらに入る
    redirect_to action: :index and return
  end

  # バリデーションに失敗したら(いずれかのStepが失敗したら)こちらにくる
  render :new
end

とてもシンプルに書けて良い感じです。

まとめ

Trailblazerでは、Controllerの処理をメソッド単位でOperationに分割します。
バリデーションに関する処理をContractにまとめ、そのほかの処理をStepとして記述します。
強力なStepのフロー管理により、様々な条件分岐を簡潔に書くことができます。

Trailblazerのドキュメントにより詳しい説明やサンプルコードがあるので、興味を持った方はそちらも見てみてください。

ドキュメント: http://trailblazer.to/guides/trailblazer/2.0/03-rails-basics.html
サンプルコード: https://github.com/trailblazer/guides/tree/operation-03

感想

実際にStepを書いてみると分かるのですが、実はモデルに書くべきことというのはほとんどなく多くの処理はリクエストに依存していたのだなというのが、ここ数ヶ月Trailblazerを利用して思った感想です。
モデルにはhas_manyなどのAssociationと、あとはscopeを書くぐらいで、ビジネスロジックは全てOperationに移動できます。

また、OperationはStepのまとまりとなっており、処理の流れを追うのがとても簡単です。2〜3ヶ月前に書いた自分が書いたコードが暗号と化している、なんてことも無くなります。さらに、影響範囲がOperation単位に閉じているので、仕様変更した時に変なところが壊れている不安もかなり減りました。

また、Operationはそれぞれが非常に簡単に実行できるので、テスト時にPostモデルを使いたいなと思った時に、factoryを利用しなくても本番で稼働しているプログラムでデータが作成できます。

# FactoryGirlで作成する場合
post = FactoryGirl.create(:post, title: "タイトル", body: "本文")

# TrailblazerのOperationを実行して作成する場合
post = Post::Create.(title: "タイトル", body: "本文")

上のFactoryGirlで作成する場合は、(ちゃんとその処理を書いてあげないと)メールは送られません5。一方で、下のTrailbalzerのOperationを実行した場合はStepに含まれるものが全て実行されるため、もちろんメールの送信も行われます。実データとfactoryが違う問題、または実データに近づけようとfactoryに色々書かないといけない問題から離脱することもできました。

何より嬉しいのは、われわれ利用者が知恵を絞って設計を考えなくても、決まった場所にコードを置いていくことでそれっぽく様々な恩恵を受けられるという点です。難しいことは何も考えずにTrailblazerの決めたレールに乗って、ハッピーに開発を進めましょう!

  1. 僕が作ったら負の遺産と言われるますが、きっとみなさんが作った場合は後世にまで語り継がれる素晴らしい遺産になると思います

  2. 考えなくても済むことを考えたくないという気持ち

  3. ビジネスロジックっていう単語を使うと、何をビジネスロジックとするかおじさんが現れるのが常なのですが、そんな次元ではなくスッキリします

  4. ある意味では「レールに乗っている」ということなのかもしれません

  5. モデルのafter_createでメール送信処理が書かれている場合は、ちゃんとメールも送られます

153
139
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
153
139

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?