概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Things I learned developing Ruby and Rails apps over the past 3+ years
- 原文公開日: 2017/01/30
- 著者: Filippos Vasilakis
3年以上かけて培ったRails開発のコツ集大成(翻訳)
順序は特に決まっていません。
1. トップレベルにrescue_from
を書く(permalink)
ルートのコントローラにrescue_from
を書くと、その下で発生したすべての例外をキャッチできるので非常に便利です。Webアプリにこれを追加すると、リクエスト/レスポンスのサイクルで実行されるほとんどのコードが一気に便利になります。シンプルなAPIを例に考えます。rescue_from
を使えば、レコードが見つからない(ActiveRecordがActiveRecord::RecordNotFound
をスローする)場合のアプリの振る舞いを明示的に指定できます。
rescue_from ActiveRecord::RecordNotFound do
api_error(status: 404, errors: 'Resource not found!')
end
2. コントローラにload_resource
を書く(permalink)
もうひとつのパターンは、以前同僚が使っていたのを見て以来採用しているものです。必要なリソースのフェッチをコントローラのメソッド内で行う代わりに、共通のコントローラのフィルタを使い、アクションの実行に応じてフェッチするというものです。
class UsersController
before_action :load_resource
def index
# @usersで何かする
end
def show
# @userで何かする
end
def create
# @userで何かする
end
def update
# @userで何かする
end
def destroy
# @userで何かする
end
private
def load_resource
case params[:action].to_sym
when :index
@users = paginate(apply_filters(User.all, params))
when :create
@user = User.new(create_params)
when :show, :update, :destroy
@user = User.find(params[:id])
end
end
end
これの発展版がdecent_exposureです。私自身はまだ使う機会がありませんが。
ところで、私は主に2つの理由から「よいコードは常にそれ自身が語る」(訳注: コメントを避けてコードに語らせる)という言説にあまり賛成できません。
- ある開発者にとってよいコードであっても、別の開発者にとっては悪いコードになることもあります(スタイルは人それぞれなので、それ自体が悪いのではありません)。
- 時間や予算の制約から、手早く修正してissueをクローズするしかないという状況はいくらでもありえます。最善の(そして最も自明な)ソリューションは「10倍努力する」ということがわかっていてもです。
というわけで、「コードが匂ってるな」と思ったら、恥ずかしがらずにどしどしコメントしましょう。
3. decoratorやpresenterを使う(permalink)
しばらく前から、「モデルをファットにして、その分コントローラを薄くせよ」という言説をRailsコミュニティで見かけます。「コントローラを薄くせよ」については同意しますが、ファットモデルについては同意できません。モデルもできるだけ薄くするべきであり、特殊の場合にしか使わないようなプロパティをモデルで自動生成しないことです。そのような場合はラッパークラスを使って(皆さん、これがdecoratorですよ!)必要なメソッドだけを公開しましょう。
presenterはdecoratorと似ていますが、複数のモデルを扱う点だけが異なります。
4. モデル配下のワーカーを名前空間化してafter_commit
で呼び出す(permalink)
User
というモデルがあるとしましょう。あるモデルに関連するバックグラウンドジョブの90%は、モデルの作成/更新/削除で発生します。ここでデータが変更されるからです。ここから、User::CreateWorker
、User::UpdateWorker
、User::DestroyWorker
という3つの一般的なワーカーを導き出せます。利用可能な場合にはこれらのワーカーをActiveRecordコールバックやprevious_changes
と組み合わせて使ってみましょう。ワーカーの呼び出しはafter_commit
で行います。理由についてはこちらをご覧ください。
5. PostgreSQLのarrayは、よほどシンプルでない限り使わないこと(permalink)
PostgreSQLのarrayもクールなのですが、私の経験では、時間を節約するより問題を生み出す方が多くなります。PostgreSQLのarrayを使うと(何らかのIDを保存するなど)、後でそのテーブルを見たときに必ず私の頭が爆発しました。データベースにはJOIN
というものがあるのですから、テーブルを追加するコストは高くありません。
PostgreSQLのarrayは、うんと小規模な場合にしか使わないことにします。
- テーブルに保存する要素が少数にとどまり、かつ要素の平均個数が将来増加しないことがわかっている場合(わずかな変動ならありです)
- テーブルがIDや関連付けと一切関わりを持たないことがわかっている場合
6. Postgres JSONBはいいヤツ(permalink)
PostgreSQLのarrayとは対照的に、PostgreSQLのJSONBは大好きです。データベースにスキーマがあることのメリットは明らかなので、私たちは皆スキーマを持つデータベースが大好きです。しかしながら、スキーマを事前に予測できない場合、スキーマレスデータベースのシンプルさがどうしても必要になることがあります。私は次のような場合にJSONBを使うことがよくあります。
- 小さな属性を多数使うことがあり、しかも親属性で名前空間される可能性もある場合。普通のテーブルでこれをやると、カラムだらけになってしまいます。
- 保存する内容が正確にわからない場合や、プロトタイプを急いで作る場合
- オブジェクトのhydrationを作る場合: オブジェクトをJSON形式でデータベースに保存し、同じJSONからオブジェクトを再構成する
訳注: hydrationは化学用語の「水和/水和物」の借用で、シリアライズに少し似た概念です。
7. aasm gemはいいヤツ、ただしステートを変えて初期化しないこと(permalink)
私はaasm gemが大好きです。ステートマシンで状態や操作を強制することができ、専用のきわめてシンプルなDSLを使えます。ただし、オブジェクトを初期状態と異なるステートで作成するとフックが動作しないという問題が生じます。aasmの内部状態とにらめっこして頑張るか、あきらめてオブジェクトの特定のステートを手動でスキャンすることになります(それ用のサービスを作ったりとか)。
8 .メールアドレスのバリデーションを頑張るよりgemを使う(permalink)
メールアドレスのバリデーションに使う正規表現をググると毎回違う正規表現が見つかるのは、もう笑うしかありません。完璧な正規表現を探すのはとっととあきらめて、おとなしくgemを使うに限ります。
9. decoratorやpresenterを使って、ビューに渡すインスタンス変数をなるべく1つだけにする(permalink)
私がRailsで残念に思っている部分です。コントローラからビューにコンテキストを渡すのにインスタンス変数をいくつも使うのは、バッドプラクティスだと思います。Sandi Metzの言うとおり、インスタンス化して渡すオブジェクトは常に1つだけにすべきです。
10. モデルに保存するインスタンスメソッドに!
を付ける(permalink)
モデルのメソッドがオブジェクトを変更してデータベースに保存する場合、メソッド名の末尾に必ず!
を付けてそのことを示しましょう。簡単なことです。
クラスレベルで厳密なAPIを書くことがコードの品質を高めることにつながりますが、開発者はそのことを忘れがちです(私もですね!)。
11. 単に認証したい場合はDevise gemを使わないこと(permalink)
Deviseはマジックが多すぎます。
12. Virtusを使って、ActiveRecordでないモデルの属性をより厳密に定義する(permalink)
私はVirtus gemを多用していましたし、今も使っています。シンプルなPORO(素のRuby: Pure Old Ruby Object)でモデルのように振る舞うオブジェクトを構成でき、属性をある程度厳密に保つこともできます。私は、属性が増えすぎたときに次のようなVirtus向けの独自DSLを書いて属性を操作できるようにすることがよくあります。
# シリアライザなどに定義した属性を再利用できるシンプルなモジュール
module VirtusModel
extend ActiveSupport::Concern
included do
include Virtus.model
if defined?(self::ATTRIBUTES)
self::ATTRIBUTES.each do |group|
group[:attrs].each do |attr|
attribute(attr, group[:type])
end
end
end
end
class_methods do
def all_attributes
self::ATTRIBUTES.map{|i| i[:attrs]}.flatten
end
end
end
# モデルの例
class Model < ActiveModelSerializers::Model
ATTRIBUTES = [
{
attrs: [
:id, :name, :header_text, :is_visible, :filtering_control,
:data_type, :description, :caregory, :calculation
],
type: String
},
{
attrs: [
:display_index, :min_value, :max_value, :value_type,
:number_of_forcast_years
],
type: Integer
},
{
attrs: [:category], type: Array
},
{
attrs: [:is_multi_select],
type: Virtus::Attribute::Boolean
}
].freeze
include VirtusModel
end
さまざまな属性の種類を列挙することも、属性のグループにある種のタグを追加することもできます。おかげで私はニッコニコです。
なお、Railsのattributes APIができたので、これで同じか似たようなことができるのではないかと考えています。
13. 外部API参照などの重たい処理にはメモ化(memoization)を使う(permalink)
もうおわかりですよね。
14. PostgreSQL全文検索は単純な用途に向いている(permalink)
pg_searchは驚くほど簡単にセットアップできます。tvectorsなどでPostgreSQL全文検索を最適化しなければならない場合は、素直にElasticSearchを使いましょう。PostgreSQLでそれ以上時間をかけるのは無駄です。
15. 2017年にもなって未だにService Objectとは何かがちゃんと定義されていない(permalink)
多くの人が同意してくれるService Objectのもっと明確な定義と、どのように実装すべきかを今も探し続けています。
私たちが最近手がけた案件では、あるパターンに従うことで再利用が楽になりました。最初に、モジュールを1つ作成します。これをinclude
すると、perform
という名前のクラスメソッドを作成します。
次に、作成するすべてのサービスで、コンストラクタ(initialize
)をprivate
にします。つまり、このperform
パブリッククラスメソッドだけを呼ぶということです(もちろんRubyのような動的言語ではprivateメソッドも呼ぼうと思えば呼べますが、単に呼びにくくするだけの処置です)。
module PerformerService
def self.included(base)
base.send(:define_singleton_method, :perform) do |url|
begin
return self.send(:new, url).send(:perform)
rescue Exception => e
Rails.logger.error("#{self.class}: Exception raised: #{e}")
end
return nil
end
end
end
class UrlParser
include PerformerService
private
def initialize(url)
@url = url
end
def perform
# ここですごいことをやる
end
end
UrlParser.perform('https://kollegorna.se')
16. ActiveRecordのエラーメッセージを好みの形に変換する(permalink)
RailsでAPIを書くと、エラーメッセージはたいていJSONAPI形式に従います。つまり、メッセージ(can't be blank
)とメッセージが失敗した属性(user_id
)が出力されます。
この例ではJSONポインタを使っていませんが、これにも同じアイデアを適用できます。
クライアント側では好みに応じて次の2つの方法でこれらを扱います。フォームに移動してuser_id
inputを赤で表示するか、メッセージを連結して「User id can’t be blank」などのように読みやすい形に変換するかです。
しかしメッセージに関連する属性がユーザーにとって意味のないものである場合はどうなるでしょうか。
このアプリで、各ユーザーは新しい投稿(post)を1つ作成できるとします。ただし投稿は1日1回までだとします。モデルで次のようにして一意性を強制します。
validates :user_id, {
uniqueness: {
scope: :post_id,
conditions: -> { where('created_at >= ?', 1.days.ago) },
}
}
(はい、DBレベルでも同じように一意性制約をかけるべきですよね、わかっております。しかしここでは仮に、ユーザーが2つの異なるサーバー(しかも同じアプリが動き、同じDBを使っている)にアクセスして、運よく(運悪く)2つのリクエストを完全に同時に受け取れないと困るので、このエラーは扱いません)
このときのメッセージは次のようになります。
{
"title": "リクエストを処理できませんでした",
"message": "(エラーの詳しい説明)",
"errors": [
{
"attribute": "user_id",
"message": "は既に使われています"
}
]
}
ユーザーはこれを渡されても困ってしまいます。1つの方法は、message
オプションを使うことです。
validates :user_id, {
uniqueness: {
scope: :post_id,
conditions: -> { where('created_at >= ?', 1.days.ago) },
},
message: 'さんの投稿は1日1回までです'
}
これで、メッセージは['user_id', 'さんの投稿は1日1回までです']
のように多少読みやすくなりましたが、両方の属性を使う場合にあまり便利ではありません。
{
"title": "リクエストを処理できませんでした",
"message": "(エラーの詳しい説明)",
"errors": [
{
"attribute": "user_id",
"message": "さんの投稿は1日1回までです"
}
]
}
理想は、このメッセージをbase
に移動することです。このメッセージは特定のモデル属性に依存しない、より一般的なカスタム制約だからです。これは、メッセージにカスタムDSLを追加すればできるようになります。
validates :user_id, {
uniqueness: {
scope: :post_id,
conditions: -> { where('created_at >= ?', 1.days.ago) },
},
message: {
replace: "user_id",
with: {
attribute: "base",
message: "ユーザーの投稿は1日1回までです"
}
}
}
def replace_errors(errors)
errors_array = []
errors.messages.each do |attribute, error|
error.each do |e|
if e.is_a?(Hash) && e[:replace]
errors_array << {
attribute: e[:with][:attribute],
message: e[:with][:message]
}
else
array_hash << {attribute: attribute, message: e}
end
end
end
return errors_array
end
これで、使いたい属性に合うエラーが出力されます。
{
"title": "リクエストを処理できませんでした",
"message": "(エラーの詳しい説明)",
"errors": [
{
"attribute": "base",
"message": "ユーザーの投稿は1日1回までです"
}
]
}
17. 値を返すメソッドでは明示的にreturn
を書く(ワンライナーであっても)(permalink)
Rubyコミュニティはreturn
文を書かないことにこだわっていると思いますが、私はそこにこだわる理由はない気がしています。実際私は、たとえワンライナーであっても、副作用が目的ではなく戻り値を目的とすべき場合はreturn
文を追加しています。
Rubyのクールさと表現力を云々することよりも、生産性と(ある種の)安全性の方が勝ります。
18. なるべくかっこ()
を使う(ある種のDSLを使う場合を除く)(permalink)
これも同様です。かっこを追加して困ることはありませんし、普段他の言語も使っている同僚が幸せになれます。
19. env変数に厳密な論理値型を追加する(permalink)
私はconfig/sercrets.yml
で次のようなスニペットを使うのが好きです。
<%
booly_env = ->(value) {
return false if value.blank?
return false if (['0', 'f', 'false'].include?(value.to_s.downcase))
return true if (['0', 't', 'true'].include?(value.to_s.downcase))
return true
}
%>
こうすることで、論理値型のenv変数がtrue
かfalse
のどちらかだけを取るようになるので、コードで使いやすくなります。
development:
enable_http_caching: <%= booly_env[ENV["ENABLE_HTTP_CACHING"] || false] %>
20. PostgreSQL以外のデータベースをメインで使うのであれば十分な理由付けが必要(permalink)
MongoDBはひと頃もてはやされていましたが、ほどなくしてMongoDBの欠点が知られるようになりました。
- スキーマレスである: スキーマレスは機能の1つだと思うかもしれませんが、実際には大きな欠点です。データベースにスキーマがあることで、スキーマを必要に応じて少しずつ変更できますし、ツールや保証も得られます。たとえば、SQLにinteger型のカラムが1つあるとすると、これをstring型やtext型に変更することも、デフォルト値の設定や
NULL
禁止の設定もできます。これはスキーマレスなデータベースでは不可能であり、プログラミング言語を用いて高度なレベルで自作する必要があります。スキーマレスなデータベースでは、属性の追加や削除も不可能です。基本的に最初のスキーマに縛られてしまうので、一から作り直して正しく移行できることを自力で確認するか、アプリケーションレベルで扱うことになります。 - トランザクションが使えない
- ACIDでない
- クエリが少し大きくなったときの速度も大したことはないと感じる
メインで使っているデータベースでこんな目に遭っても構いませんか?私はイヤです。個人的にMongoDBの唯一の目玉機能と思えるのは、親ドキュメントに多数のドキュメントを埋め込めることぐらいです。それ以外の機能はおそらくPostgreSQLで用が足ります(それにセキュリティアップデートの面倒を見なければならないデータベースシステムが1つで済みます)。
21. 動的スコープは、他に打つ手がない場合にはよいパターン(permalink)
Rubyでクロージャ(proc
やlambda
)を定義すると、レキシカルなスコープや環境がクロージャにカプセル化されます。
これは、コードのAという場所でprocを定義したとしても、コードのBという場所でそれを渡して呼び出したときに、procが定義されたAのレキシカルスコープ内で定義されているものであれば変数でも何でも参照できるということです。言い方を変えると「環境について閉じている」ということです。
これを逆にしたらどうなるでしょうか。たとえばコードのAという場所でprocを1つ定義し、そこでprocを呼んでもまったく意味がないが、コードのBという場所でprocを呼びたい場合にクロージャのレキシカルスコープを変更することで、実行結果にBの環境が反映されるようにするとします。
次の例をご覧ください。
CLOSURE = proc{puts internal_name}
class Foo
def internal_name
'foo'
end
def closure
proc{puts internal_name}
end
def name1
closure.call
end
def name2
CLOSURE.call
end
end
puts Foo.new.name1 #=> foo
puts Foo.new.name2 #=> undefined local variable or method `internal_name' for main:Object (NameError)
クロージャの定義時点ではinternal_name
が定義されていないので、当然name2
メソッドは失敗します。
しかし、instance_exec
を使うとprocのバインディング(レキシカルスコープ)を再定義できます。
CLOSURE = proc{puts internal_name}
class Foo
def internal_name
'foo'
end
def closure
proc{puts internal_name}
end
def name1
closure.call
end
def name2
instance_exec(&(CLOSURE))
end
end
puts Foo.new.name1 #=> foo
puts Foo.new.name2 #=> foo
成功です。これは、アプリのある部分に書いたコードを、まったく異なるコンテキストで実行できるということです。しかしこれはどんなときに便利なのでしょうか?このあたりをいろいろハックしてみましたが、非常に有用な使いみちの1つはRailsのルーティングでした。
次のようなルーティングがあるとします。
namespace :api do
namespace :v1 do
resources :company_users, only: [:show] do
resources :posts, only: [:index] do
resource :stats, only: [:show]
end
end
end
end
上から以下のルーティングが生成されます。
/api/v1/company_users/:id
/api/v1/company_users/:company_user_id/posts
/api/v1/company_users/:company_user_id/posts/:post_id/stats
:company_user_id
はどうやら不要なので、次のようにしてクライアント側での柔軟性を高めたいと思います。
/api/v1/stats?user_id=:company_user_id&post_id=:post_id
しかしAPIは既に本番で稼働していて変更は困難です。
namespace :api do
namespace :v1 do
resources :company_users, only: [:show] do
resources :posts, only: [:index] do
resource :stats, only: [:show]
end
end
resource :stats, only: [:show], defaults: {company_user_id: proc{params[:company_id]}}
end
end
ルーティングの中にparams
がある?そのとおり!理由は、次のスニペットを使って、procのコンテキストをコントローラのコンテキストに再バインドしているからです。
def reshape_hash!
self.params = HashWithIndifferentAccess.new(params.to_unsafe_h.reshape(self))
end
これで、このルートにuser_id
を送信すると、このメソッドをbefore_filter
として追加することで、company_user_id
として追加されます。
class Api::V1::StatsController < ApplicationController
before_action :authenticate_user!
before_action :reshape_hash!
def index
stats = Stats.new(current_user).all(
user_id: params[:company_user_id], post_id: params[:post_id]
)
render json: stats, serializer: StatsSerializer
end
...
このテクニックをルーティング以外で使ったこともありますが、ほとんどは最後の手段としてです。ご利用は計画的に。