rails-style-guideを読む①
railsのアプリを本格的に作る前に、今回からスタイルガイドを見ていこうと思います。
まだ難しいですが、汚いコードを量産するのは嫌なので、この段階で読むことにしました。
rails-style-guideは長いです。なので、記事を分割します。分割しても長いです。
この記事は10000文字以上あるので、気軽には読めませんが、rails-style-guideは難しいからまだ読んでいないという方はぜひお付き合いください。
英語で読むのはめんどくさいので、githubに置いてある日本語訳を読みます。
本文をそのまま載せることはしていません。オレオレ噛み砕き文章に改変しています。本物を読みたい方は各自参照をお願いします。
コードは基本そのまま持ってきますが、ちょいちょいコメントを入れたりしてます。
今回は見るのは、
・序曲 ・設定 ・ルーティング ・コントローラ ・モデル
です。
序曲
RSpecとSassとSlimを使いましょうってことですね。わかります。
このブログでアプリを作るときは、cucumberも使いたいなぁ。
設定
ここはコードがないので駆け足気味で。
初期化をまとめると、
・アプリの初期化は、config/initializersに書く ・gemごとの初期化は、gemと同じ名前のファイルを作って書く
環境の設定は、
・すべての環境に適用する設定をconfig/application.rbに書く ・各環境の個別の設定をconfig/environmentsの下にファイルを作って書く ・staging環境を追加で作る
こんな感じか。ちゃんと設定を書ける男になりたい。
ルーティング
ここはコードがたくさんあるので、丁寧に見ていきます。
・RESTfulリソースにアクションを追加するときは、"member"か"collection"ルートを使う
# 悪い <- memberもcollectionも使ってない get 'subscriptions/:id/unsubscribe' resources :subscriptions # 良い <- member使用 resources :subscriptions do get 'unsubscribe', on: :member end # 悪い <- memberもcollectionも使ってない get 'photos/search' resources :photos # 良い <- collection使用 resources :photos do get 'search', on: :collection end
resourcesは、scaffoldしたときに生成されるメソッドたち(index, show, newとか)のルートと、コードの中で使えるパスとオプションを生んでくれるようです。
詳しくは、ここをどうぞ
memberとcollectionの違いは、
memberが一つのインスタンス、collectionはすべてのインスタンスのアクションを書くようです。
上の例でも、memberの悪い方の例のURLには:idが入ってるけど、collectionには入ってないですね。
・複数のmember/collectionルートを定義するときは、ブロックで書く
resources :subscriptions do member do get 'unsubscribe' # more routes end end resources :photos do collection do get 'search' # more routes end end
そりゃそうですね。毎回行末にon :memberとか書いてらんないし、読みにくいですものね。
・ActiveRecordモデルの関係を表現するため、ルートはネストで書く
class Post < ActiveRecord::Base has_many :comments end class Comments < ActiveRecord::Base belongs_to :post end # routes.rb resources :posts do resources :comments end
モデルに関係を定義したら、それに応じたルートの書き方をしましょうってことですね。
・関連するアクションをグループ化するためにnamespaceを使用する。
namespace :admin do # Directs /admin/products/* to Admin::ProductsController # (app/controllers/admin/products_controller.rb) resources :products end
namespaceは、任意のディレクトリで機能を分けたいときに使うらしい。
上のコードでは、admin以下に、productsのresourcesで生まれるルートが生成される。要するに、関連するアクションはディレクトリを切りましょうってことか。
・古い記法のワイルド・コントローラ・ルートを使わない。
# とても悪い <- 古い記法 match ':controller(/:action(/:id(.:format)))'
基本的に「この書き方は廃止」っていう部分は載せない方針で行こうと思っていたのですが、これは有名みたいなので載せました。
この書き方をすると、すべてのアクションをGETメソッドで実行できちゃうみたいです。なので、めんどくさがらず、resourcesを使ってしっかり全部書きましょうってことかな。
・matchは廃止された
これも上のつながりで載せました。ググった時にmatchが出てきても使っちゃダメよと注意喚起です。
ここによると、ワイルドカードを封じて、get, postを使ってちゃんと書いて欲しいみたいですね。
ってことは、どっちにしろ上のワイルド・コントローラ・ルートは使えないってことですね。
コントローラ
ここにはコードが無いため、駆け足気味に行きます。
コントローラで大事な考え方は、ビュー層のためのデータを取り出すだけのものであるってことです。
・コントローラにはロジックは含めない。ロジックはモデルに書きましょう ・アクションは、初期のfindとnew以外には、1つのメソッドだけを起動する ・コントローラとビューの間の変数の共有は2つまで
ロジックはモデルに入れるんですね。イメージではコントローラだと思ってました。危ない危ない。
次のfindとnew以外には1つのメソッドを起動すべきっていうのは、ルーティングされたURLのアクション以外は起動しないように書けってことでいいのかな?アクションの中で他のアクションを呼ぶような書き方はダメってことですかね。
コントローラとビューの変数の共有を2つに絞るのは、複雑にしないためですかね。
モデル
ここはあまりに巨大なので、小分けに見ていきます。
まずはコードのない部分の要約を。
・非ActiveRecordモデルのクラスは自由に導入していい ・モデルには、略語のない、意味のある、短い名前をつける
ActiveRecordでないクラスは自由に書いてよくて、モデル名は頑張って考えましょうと。
・ActiveRecordのバリデーションのようなことをやるときは、ActiveAttr gemを使う
コードの完全な例はRailsCast on the subject を参照。
# ActiveRecord::Baseを継承していないクラス class Message include ActiveAttr::Model # attributeを設定 attribute :name attribute :email attribute :content attribute :priority # name, email, contentをハッシュで渡せるように # Message.new(name: 'hoge', email: 'hoge', content: 'hoge')で行ける attr_accessible :name, :email, :content # name, email, contentにvalidationを入れる validates :name, presence: true validates :email, format: { with: /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i } validates :content, length: { maximum: 500 } end
ActiveRecordを継承してないモデルでActiveRecordと同じようなvalidationを書きたいときは、ActiveAttrっていうgemを使いましょうってことか。
非ActiveRecordのモデルをバンバン書く日が果たしていつ来るのか…頑張りましょう。
ここからは、ActiveRecordについてです。
・十分な理由がない限り、ActiveRecordのデフォルトを変更しない(テーブル名、主キーなど)
# 悪い - スキーマを修正できるのなら、このようにしないでください。 class Transaction < ActiveRecord::Base # テーブル名を変更してる self.table_name = 'order' ... end
テーブル名はRailsのルールを守んないと、後でわけわかんなくなりますよね。
・マクロスタイルのメソッドはクラス定義の初めにまとめる。マクロスタイルも一定の順番で書く
class User < ActiveRecord::Base # デフォルトスコープは最初に(あれば) default_scope { where(active: true) } # 続いて定数 GENDERS = %w(male female) # その後attr関係のマクロを置きます attr_accessor :formatted_date_of_birth attr_accessible :login, :first_name, :last_name, :email, :password # 関連マクロが続きます belongs_to :country has_many :authentications, dependent: :destroy # そしてバリデーションマクロ validates :email, presence: true validates :username, presence: true validates :username, uniqueness: { case_sensitive: false } validates :username, format: { with: /\A[A-Za-z][A-Za-z0-9._-]{2,19}\z/ } validates :password, format: { with: /\A\S{8,128}\z/, allow_nil: true} # 次にコールバックです before_save :cook before_save :update_username_lower # その他のマクロ(deviseなど)はコールバックの後に置かれるべきです ... end
マクロスタイルのメソッドの順番は、
・デフォルトスコープ ・定数 ・attr ・関連 ・バリデーション ・コールバック ・その他
こういうルールはしっかり守らないとダメですよね。早く「普通に書いたらこの順番になる」ようになりたいところです。
・has_and_belongs_to_manyより、結合モデルに対して、追加の属性とバリデーションを追加する、has_many :throughを使う。
# has_and_belongs_to_many を使用 class User < ActiveRecord::Base has_and_belongs_to_many :groups end class Group < ActiveRecord::Base has_and_belongs_to_many :users end # 好ましい方法 - has_many :through を使用 class User < ActiveRecord::Base has_many :memberships has_many :groups, through: :memberships end class Membership < ActiveRecord::Base belongs_to :user belongs_to :group end class Group < ActiveRecord::Base has_many :memberships has_many :users, through: :memberships end
has_and_belongs_to_manyは、多対多の関係を表すようです。
全然Railsの考え方を身につけてない身からすると、has_and_belongs_to_manyの方が直感的に見えます。
ただ、下の例だと、Membershipに属性とバリデーションが入るんですね。
試しに両方のRailsアプリを作ってコンソールでメソッドを見てみたところ、throughの方がメソッドが多かったです。気になる方は試してみてください。
簡単に言ってしまえば、多対多で相互に所有しあう関係のときは、
has_many 相手, through: 中間クラス
と書く癖をつけましょうってことですかね。
・read_attribute(:attribute)ではなく、self[:attribute]を使う
# 悪い def amount read_attribute(:amount) * 100 end # 良い def amount self[:amount] * 100 end
わざわざメソッドを使って呼ばずに、ハッシュのキーみたく呼ぼうってことですかね?
・バリデーションの書き方は、最新のsexy validationsに合わせる
# 悪い validates_presence_of :email # 良い validates :email, presence: true
sexy validationsには、こんな例がありました。
class Film < ActiveRecord::Base validates :title, :presence => true, :uniqueness => true, :length => { :maximum => 100 } validates :budget, :presence => true, :length => { :within => 1..10000000} end
これからRailsを始めるなら、sexy validationsにある書き方から覚えていけば良さそうですね。
・カスタムバリデーションが以下に該当する場合、カスタムバリデータファイルとして外に書く。カスタムバリデータは、app/validatorsに置く。
・カスタムバリデーションを2回以上使う ・バリデーションで正規表現を使用している
それではコードを。
# 悪い <- 正規表現を使ってる class Person validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i } end # 良い <- 正規表現を外出し class EmailValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i end end class Person validates :email, email: true end
これは、関数にするか否かの判断基準と同じようなものですね。DRYと複雑性に対応する基準を明文化してくれてるので、オレオレ基準はやめましょうってことか。
もっと抽象化する指針として、こんなもんもありました。
・複数の関連するアプリケーションをメンテしてるか、バリデータが十分に一般的なときは、共有するgemにカスタムバリデータを抽出することも視野に入れる。
まだ、gemに抽出するのに「十分な」一般性がどんなもんかよくわからないので、慣れてきたらまたこれについて考える時が来ると思います。
・named scopeは自由に使う。
scopeは検索メソッドを追加するようです。コードを見たほうがわかりやすいので、コードを見てみましょう。
class User < ActiveRecord::Base scope :active, -> { where(active: true) } scope :inactive, -> { where(active: false) } scope :with_orders, -> { joins(:orders).select('distinct(users.id)') } end
上のコードは、以下のメソッドを作ります。
・User.active -> Userテーブルの中の、activeがtrueのレコードの配列を返す ・User.inactive -> Userテーブルの中の、activeがfalseのレコードの配列を返す ・User.with_orders -> UserテーブルとOrderテーブルのを合わせて、user_idで分ける
・named scopeはlambdaで包む
# 悪い class User < ActiveRecord::Base scope :active, where(active: true) scope :inactive, where(active: false) scope :with_orders, joins(:orders).select('distinct(users.id)') end # 良い class User < ActiveRecord::Base scope :active, -> { where(active: true) } scope :inactive, -> { where(active: false) } scope :with_orders, -> { joins(:orders).select('distinct(users.id)') } end
これは、とりあえずlambdaで囲っておけばいいということですかね。
・引数付きのlambdaを使ったnamed scopeが複雑になる場合、ActiveRecord::Relationsオブジェクトを返すクラスメソッドを作ることで、単純なscopeを定義できる。
class User < ActiveRecord::Base def self.with_orders # joinsはActiveRecord::Relationsオブジェクトを返す。 joins(:orders).select('distinct(users.id)') end end
マクロで書くときのscopeは、とにかく単純な書き方でいったほうがいいみたいですね。
複雑になりそうなときはクラスメソッドとして書きましょうと。
ここからは、わかりやすいURLを書く方法についてです。
・人が見てわかりやすいURLを使うよう心がける。URLの中では、モデルのidではなく、記述的な属性を使う。
・to_paramメソッドをオーバーライドする。デフォルト実装ではレコードのidを返すから、人が見てわかる属性を入れる。
class Person def to_param # idが先頭にあるのは、ActiveRecordのfindメソッドで見つけることができるようにするため。 # parameterizeはURLフレンドリーな値に変換する "#{id} #{name}".parameterize end end
parameterizeがどんなものか試してみました。
> "1 kenta".parameterize => "1-kenta"
スペースをハイフンにしてくれました。スペースはURLの敵ですものね。
・idの代わりに記述的な属性を使ってURLを生成する、friendly_id gemを使う。
friendly_idの詳しい書き方はここを参照。
class Person extend FriendlyId friendly_id :name, use: :slugged # かぶらないようなslug_candidatesを書く def slug_candidates [ :name, [:name, :birthday], [:name, :sex, :birthday], [:name, :age, :sex, :birthday] ] end end
以上でrails-style-guideを読む記事の第1回を終わります。
まだ全体の3分の1くらいですので、rails-style-guideは全3回の予定です。
難しいし、長いしでかなりしんどいかと思いますが、スタイルガイドは大事ですし、最後まで頑張りましょう。