込みいった生成処理をファクトリメソッドに置き換える
元記事はこちら
翻訳
Bad Smell
class InvoicesController < ApplicationController def create @invoice = Invoice.new(params[:invoice]) @invoice.address = current_user.address @invoice.phone = current_user.phone @invoice.vip = (@invoice.amount > 1000) if Time.now.day > 15 @invoice.delivery_time = Time.now + 2.month else @invoice.delivery_time = Time.now + 1.month end @invoice.save end end
invoice(請求書)の生成メソッドは込み入っていて、InvoicesControllerが読みづらくなっています。 それに、コントローラーはモデルの生成方法について知りすぎるべきではなく、請求書の生成ロジックはInvoiceモデルに移すべきです。
Refactor
class Invoice < ActiveRecord::Base def self.new_by_user(params, user) invoice = self.new(params) invoice.address = user.address invoice.phone = user.phone invoice.vip = (invoice.amount > 1000) if Time.now.day > 15 invoice.delivery_time = Time.now + 2.month else invoice.delivery_time = Time.now + 1.month end end end class InvoicesController < ApplicationController def create @invoice = Invoice.new_by_user(params[:invoice], current_user) @invoice.save end end
new_by_userメソッドをInvoiceモデルに定義し、請求書の作成を担当させます。 こうすることでInvoicesControllerは請求書オブジェクトを得るためにnew_by_userメソッドを呼ぶだけでよくなりました。 “Skinny Controller, Fat Model"原則を忘れないようにしましょう。
感想
最近書いているアクションはほとんど5行以内におさまっていて、非常に読みやすい。 Fat Modelに関しては様々意見があるとは思うけれど、どこまでFatを許容するかは人(プロジェクト)それぞれだと思う。 いくら原則に従っていても、太りすぎたモデルは読みづらいし、何かしらのストレスを感じるほどに太っているコードは リファクタリングするべきというサインじゃないかな。 特にRailsで開発しているのであれば、Modelの肥大化に伴って、テストも肥大化して行くはずだし。 その辺のバランスをどういう風にとっているのか、プラクティスがしりたい。 ここによると100行が一つの基準になる?
何かで読んだ記憶もある気がする。。
リファクタリングだったか、レガシーコード改善ガイドだったか。。。
モデルのロジックをモデルに移動する
元記事はこちら
翻訳
Bad Smell
class PostsController < ApplicationController def publish @post = Post.find(params[:id]) @post.update_attribute(:is_published, true) @post.approved_by = current_user if @post.created_at > Time.now - 7.days @post.popular = 100 else @post.popular = 0 end redirect_to post_url(@post) end end
上記の例では、PostsControllerはpostを公開したいので、 まずpostを検索し、 postのis_published属性とapproved*1に値をセットし、 popular属性にcreated_atの値をもとに値を設定してと、 PostsControllerはロジックを知りすぎており、コントローラーの責務ではなく、モデルが操作するべきです。
Refactor
class Post < ActiveRecord::Base def publish self.is_published = true self.approved_by = current_user if self.created_at > Time.now - 7.days self.popular = 100 else self.popular = 0 end end end class PostsController < ApplicationController def publish @post = Post.find(params[:id]) @post.publish redirect_to post_url(@post) end end
公開ロジックをコントローラーからモデルに移動してpublishメソッドをPostモデルを作ったので、コントローラーからpublishメソッドを呼ぶだけでよくなりました。 これで綺麗になりました。
感想
モデル内でcurrent_userを使っているのが気になるなぁとおもった。
*1:approved_byの誤り?
モデルに仮想的な属性を追加する
元記事はこちら
翻訳
Bad Smell
<% form_for @user do |f| %> <%= text_field_tag :full_name %> <% end %> class UsersController < ApplicationController def create @user = User.new(params[:user]) @user.first_name = params([:full_name]).split(' ', 2).first @user.last_name = params([:full_name]).split(' ', 2).last @user.save end end
上記の例では、full_name入力欄がある画面ですが、データベースには姓、名を分けて保存したいので、コントローラーで姓と名を分割して@userオブジェクトに設定しています。 しかし、モデルに属性を設定するのは、コントローラーの責務ではありませんから、仮想的な属性により、この処理をモデルにさせるべきです。
Refactor
class User < ActiveRecord::Base def full_name [first_name, last_name].join(' ') end def full_name=(name) split = name.split(' ', 2) self.first_name = split.first self.last_name = split.last end end <% form_for @user do |f| %> <%= f.text_field :full_name %> <% end %> class UsersController < ApplicationController def create @user = User.create(params[:user]) end end
こうすることで、Userモデルにfull_name=
という仮想的な属性が付加され、姓と名を設定するするようになりました。
こうすれば、たった1行でuserを作成することができるようになり、Userモデルの中で姓と名を分割できるようになるので、いい感じになります。
感想
分割して入力された電話番号をつなげてDBに放り込みたいとか、例とは逆の方向でなら使う機会は多そう。
Railsガイドによると、confirmation: true
な時にも仮想属性が生まれてるみたい。
スコープを使ってアクセスする
元記事はこちら
翻訳
オブジェクトのオーナーとcurrent_userを比較することで、アクセス権をチェックしたいとおもったら、そんな冗長でイケてない書き方はせずに、スコープをつかてアクセスしましょう。
Bad Smell
class PostsController < ApplicationController def edit @post = Post.find(params[:id]) if @post.user != current_user flash[:warning] = 'Access denied' redirect_to posts_url end end end
上記の例では、postのuserをcurrent_userと比較して、一致しなかった場合に、postの編集を許可しないようにしている。 しかし、このやり方は、edit,update,destroyなどを実行できるかチェックするには冗長すぎる。スコープを使ってリファクタリングしよう。
Refactor
class PostsController < ApplicationController def edit # raise RecordNotFound exception (404 error) if not found @post = current_user.posts.find(params[:id]) end end
こうすることで、current_user.postsにだけ紐づくpost、言い換えるとcurrent_userに紐づくことが保証されたpostを検索することができた。 紐付いたpostが存在しない場合、404エラーが返却される。 current_userとオーナーを比較する必要は全くなく、スコープアクセスを使うだけでアクセス権のチェックをより単純に書くことができる。
感想
404が返るってところは、厳密に言うとActiveRecord::RecordNotFoundがraiseされて、rescue_fromを書いていれば、ActionController::Rescueがひろう仕組みらしい。
Modelの関連付けを利用する
元記事はこちら
翻訳
Bad Smell
class PostsController < ApplicationController def create @post = Post.new(params[:post]) @post.user_id = current_user.id @post.save end end
この例では、user_idは明らかに@postに割り当てられます。大きな問題にはなりませんがModelの関連付けを利用すれば、この行は書かずに済みます。
Refactor
class PostsController < ApplicationController def create @post = current_user.posts.build(params[:post]) @post.save end end class User < ActiveRecord::Base has_many :posts end
userとpostの間に1対多の関連付けを定義したので、current_user.posts.build または current_user.posts.create によってpostを生成することができますし、current_userのidはactiverecordによって自動的にpostのuser_id属性に割り当てられます。
感想
動かすには
class Post < ActiveRecord::Base belongs_to :user end
が必要かな。
正直このへんはよくわかっていないので整理。
Active Record の関連付け (アソシエーション) | Rails ガイド
アソシエーション | 関連 | 特徴 |
---|---|---|
belongs_to | 1:1 | 外部キーを持つ方のモデルに設定する。 |
has_one | 1:1 | 外部キーを持たれる方のモデルに設定する。 |
has_many | 1:n | 外部キーを持たれる方のモデルに設定する。 |
has_many :through | n:n | :through 以下には中間テーブルの名前が入る |
has_one :through | 1:1 | :through 以下には中間テーブルの名前が入る |
has_and_belongs_to_many | n:n | 中間テーブルのmodelを持たない。(DBにテーブルは必要) |
リレーションシップのモデルそれ自体を独立したエンティティとして扱いたい(両モデルの関係そのものについて処理を行いたい)のであれば、中間に結合モデルを使用するhas_many :throughリレーションシップを選ぶのが最もシンプルです。リレーションシップのモデルで何か特別なことをする必要がまったくないのであれば、結合モデルの不要なhas_and_belongs_to_manyリレーションシップを使用するのがシンプルです
ふむ。関連テーブルをmodelから意識する必要がなければhas_and_belongs_to_many
を使うと。