Railsアプリケーションの実装で気をつけている8つのこと
k-shogo
この記事は RECRUIT MARKETING PARTNERS Advent Calendar 2018 の投稿記事です。
12月はRubyのリリースが楽しみなk-shogoです。
今までに規模も寿命も様々なRailsアプリケーションの開発に携わってきました。本記事ではそんな自分が「Railsプロジェクトにかかわるならこんな方針を合意できるチームが良いな」と思っていることをまとめます。
どんなことに気をつけているのか
Railsでアプリケーションを作成する時、もしscaffoldで事足りるようなものならば取り決めは必要にはなりません。
複雑なアプリケーションだったとしても、一人で開発しコードが全て頭に入っており、今後もずっとメンテナンスできる覚悟があり、過去の自分を常に信頼できるのであればこれもまた方針は不必要です。
コードの規模が大きくなりそう、サービスの寿命が長くなりそう、複数人で開発を進めるなどの状況の場合はチームで実装方針を決めておくことは有用です。
特に機能を追加/変更する時や、新しい開発チームメンバーが加わってコードを読み解く時などにおいて「どこを読む、どこにどうやって書く/変更する」が明確になるからです。
テーブルには状態を持たせず、代わりに分割できないか検討する
例えばあなたのアプリケーションにユーザー認証の機能を持たせたとしましょう。
ユーザーを一意に定めるための情報はメールアドレス(emailカラム)にすることとして、ユーザー情報を格納するテーブルは以下のようなものができるでしょう。
class CreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
t.string :email, null: false
t.string :encrypted_password, null: false
t.timestamps null: false
end
add_index :users, :email, unique: true
end
end
ユーザー認証の機能を持たせた時、往々にしてあるのが「退会者は物理削除ではなく、論理削除にしておいて」なんてオーダーです。
「削除フラグを持たせよう」「deleted_at
カラムに退会時刻を持たせたら?」「退会以外の状態も持たせたいからstatus
カラムで良いか」など色々な誘惑が頭をよぎりますが全て断ち切りましょう。
ユーザーテーブルの話に限らず、論理削除の注文は言葉通りに論理削除したいわけではなく、データを残しておきたいとか、復元させたい場合があるという理由によることがほとんどです。
真の理由を明確にして論理削除ではない方法で実現できないか検討しましょう。
論理削除フラグもしくは状態カラムを持たせてしまった場合、有効なデータを引いてくる際に常に状態のチェックが必要になってしまいます。これは複数のテーブルをjoinする場面や、関連(Railsだと has_many
など)を扱う場合に実装を複雑にする要因となります。
またテーブルに制約を設定しにくくなる原因にもなります。
例えば上記のユーザーテーブルの場合、ユーザーを一意に特定するために email
カラムを用いているのでここに一意制約をつけたいところです。
しかし論理削除を用いてしまうと「退会したユーザーと同じメールアドレスで新規ユーザー作成」することができなくなってしまいます。
論理削除をしたくなった場合、テーブルの分割でやりたいことを実現できないか検討しましょう。
ユーザーの例では「退会したユーザーのテーブル」を分けることです。
before_action の乱用はやめよう
コードを scaffold で生成した時、以下のように before_action
が使われています。
class ItemsController < ApplicationController
before_action :set_item, only: [:show, :edit, :update, :destroy]
private
def set_item
@item = Item.find(params[:id])
end
end
デフォルトで書かれているので、これを倣ってインスタンス変数は before_action
で用意すればいいんだと拡張するとこうなっていきます。
class ItemsController < ApplicationController
before_action :set_item, only: [:show, :edit, :update, :destroy]
before_action :set_foo, only: [:show, :edit, :update]
before_action :set_bar, only: [:edit, :update] # foo が必要なので実行順序変えないこと
end
一つ一つが独立していればまだマシですが、複数の before_action
が依存し合っているとロジックの把握が困難になり、機能追加の際などにバグを混入させる原因となります。
また、各アクションで何がインスタンス変数としてセットされるのかを読むことを難しくします。
解決策は before_action
は使わずに各アクションでセットすることです。
class ItemsController < ApplicationController
def edit
@item = Item.find(params[:id])
@foo = Foo.find(params[:foo_id])
@bar = @foo.bars.find(params[:bar_id])
end
end
メソッドに分割したくなるほど複雑な場合は「インスタンス変数にセットする」という副作用を無くして実装しましょう。
class ItemsController < ApplicationController
def edit
@item = find_item(params[:id])
@foo = find_foo(params[:foo_id])
@bar = find_bar(@foo, params[:bar_id])
end
private
def find_foo(id)
Foo.joins(:baz).merge(Baz.qux).complex.find(id)
end
def find_bar(foo, id)
foo.quux.spam.find(id)
end
end
before_action
が有効な場面
before_action
を用いるのが有効な場面もあります。
それは「認証・認可」と「ネストしたルーティング」を扱う時です。
ユーザーがログイン済みの場合しか扱えない操作だったり、必要な権限を持っているか確認する時などは before_action でチェックし、ダメなら403を返してあげるとスマートに実装できます。
class ItemsController < ApplicationController
before_action :authenticate_user! # ログインしないと使えない
end
class ItemsController < ApplicationController
before_action :check_admin # 管理権限を持っているかチェック
end
またネストしたルーティングを使って親となるリソースが存在する時は before_action で親リソースの取得を行ってしまうのも有効です。
Rails.application.routes.draw do
resources :categories do
resources :items # ネストしたルーティング
end
end
class ItemsController < ApplicationController
before_action :set_category # 親となるカテゴリーが必ず指定される
private
def set_category
@category = Category.find(params[:category_id])
end
def category
Category.find(params[:category_id])
end
end
親リソースのインスタンス変数がアクション全てで必要ないなら以下のようなメソッドでも良いので、必ずしも before_action に寄せる必要はないのでご注意ください。
class ItemsController < ApplicationController
private
def category
Category.find(params[:category_id])
end
end
認証認可でもネストしたルーティングでもどちらに置いても注意する点は、
only
, except
オプションを使い出したら「設計が間違っているかも」と疑い出すことです。
そうなってきたら before_action をやめるか、後述のコントローラー分割を検討しましょう。
コントローラーを分割しよう
コントローラーの分割は基本の方針としてかなり浸透してきたように感じています。
アクションとして定義できるのは index
new
create
show
edit
update
destroy
のみ。基本のアクション以外は定義せず、それ以外のアクションが必要な場合コントローラーは分割しようというものです。
Command Query Responsibility Segregation (コマンド・クエリ責務分離)を心がよう
ここでのコマンドとクエリとは何を指すのか簡潔にまとめると以下の定義になります。
- コマンド:オブジェクトの状態を変更し、値を返さない
- クエリ:値を返し、オブジェクトの状態は変更しない(副作用は無い)
※ スタックのpush/popなど更新と結果取得をアトミックに行わなければならないような例外もあります。
コマンド・クエリの責務分離とは、クラスレベルでコマンドとクエリを分けるようにすることです。
コマンドクラスは、run
call
execute
などのコマンドを実行するメソッドが1つだけが公開されているものです。
「サービスクラス」と似たような実装となりますが、「サービスクラスはなんでもあり」となりがちなので、コマンドの原則を守るようにしましょう。
クエリクラスは基本的に ActiveRecord::Base
を継承したクラスつまりモデルがそのまま使えますが、複雑な場合にはPOROでクエリクラスを作成します。
後述する方針もコマンド・クエリ責務分割を前提にしたものがあります。
コールバックを使わない
モデルの挙動が複雑になる原因の多くがコールバックです。
複数のコールバックが絡むと、一つのモデルの状態変更がシステム全体に及ぼす影響を把握できなくなっていきます。
CRUD のうちの CUD において、複数のモデルが関わるような複雑な操作が必要になったなら、それぞれ適切なコマンドクラスに分割しましょう。
コールバックで行わなければならないことはコマンドの中に実装できるはずです。
accepts_nested_attributes_for
は使わない、フォームオブジェクトを使う
画面上のフォームは一つだが、関連するモデルは複数ある場合などに accepts_nested_attributes_for
オプションが使われますが、これもActiveRecordを汚染していく大きな要因となります。
画面上のフォームとモデルが素直に一致しない場合、フォームオブジェクトを用意しましょう。
フォームオブジェクトでバリデーションを行い、永続化はコマンドクラスに実装します。
コンテクストに応じたフォームクラスを適切に用意することで、モデルのvalidatesに on
オプションを使ってしまってモデルを汚くすることも防げます。
非同期処理のエントリーポイントはコントローラーからのみとする
非同期処理のエントリーポイントが統一されていない状態は、コールバックの乱用と同じくアプリケーションの振る舞いを予期できなくします。
コールバックとそこから呼ばれる非同期処理の組み合わせは最悪です。
非同期処理を行いたい場面とは「時間がかかる処理を待たずにユーザーにレスポンスを返したい」という唯一の理由からしか生まれません。
したがってユーザーからのリクエストを受け取り、レスポンスを返す役目のコントローラーのみがエントリーポイントになりえます。
非同期処理を司るクラス(例えばワーカークラスとかジョブクラス)はコントローラーと同じくシンプルに保つことを心がけましょう。
非同期処理を行うクラスで行うことは変数のキューへの追加と取り出しのみで、メインの処理はコマンドクラスに実装します。
こうすることでテストの記述やユーザーのリクエストに依存しない場合の実行(rakeタスクやrails consoleでの実行)も容易にします。
可能ならモデルも分割しよう
アプリケーション利用者が複数のコンテクストを持つ場合(例えば管理者と一般ユーザーなど)、コントローラーとビューをネームスペースで分離することはよく目にします。
しかし、そういった場合でもモデルは単一で、モデルの実装が複雑になってしまっているケースにもよく遭遇しました。
コンテクストによってモデルを分割して複雑さを取り除けないか検討しましょう。
ActiveRecord::Baseのtable_name
メソッドを使えばテーブル名を任意に定めることができるので、同じテーブルを参照する複数のモデルも作れます。
おわりに
本記事ではRailsアプリケーションを開発していく際に気をつけたいことをまとめてきました。
共通して考えていることとしては、Githubのプルリクエストをレビューした時、diffを見てコード変更の影響範囲が把握できるかということです。
例えばbefore_actionやコールバックの乱用は、diffだけ見たら単純な変更でも、実は他の処理に影響を与えていたなんて事態を簡単に引き起こしてしまいます。
アプリケーションの挙動が複雑で、その理解を妨げるような乱雑なコードだと、拡張やメンテナンスには熟練を要し、新しいメンバー加入のハードルを高くしてしまいます。
そして往往にして「全部まるっと作り直してしまいたい」という思いに取り憑かれてしまうものです。
Railsには一見して便利なオプションが用意されていますが、無計画な利用はアプリケーションを蝕んでいきますのでご注意ください。
日頃から綺麗にするように心がけ、大掃除で苦労することが無いようにしたいですね。