初心者がRailsを勉強するブログ

Railsを0からお勉強するブログです。

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回の予定です。

難しいし、長いしでかなりしんどいかと思いますが、スタイルガイドは大事ですし、最後まで頑張りましょう。