if it does't challenge you, it can't change you

RubyのWebエンジニアやってます。主にRuby on Railsのことで勉強になったこと書いていきます。

【Ruby on Rails】結局メソッドってどこに書けばいいの?

Railsではモデルなんて使わなくてもコントローラやビューにメソッドを書きまくっても動かすことができます。
しかし、そんなことをしてたらMVC構造の意味がないですし、きちんと考えて書くことによって、そのコード量を劇的に減らすことができます。 メソッドをどこに定義するかは、そのメソッドをどこで呼び出したいかに依存すると思います。
今回はビューで呼び出したいメソッドを定義する場所について書いていきます。

コントローラは最低限

最近はカップルで使える家計簿アプリを作っているのですが、
同じビューを2つのコントローラで使っていました。
何も考えていなかった頃は、コントローラで計算してインスタンス変数に代入してビューで使うということをしていました。 こんな感じですね。

# expenses_controller.rb
 def index
  @current_user_expenses_of_both = Expense.ones_expenses_of_both(current_user)
  @partner_expenses_of_both = Expense.ones_expenses_of_both(partner)
  @sum = @current_user_expenses.sum(:amount)
  @both_sum = Expense.must_pay_this_month(current_user, partner)
  @category_sums = Expense.category_sums(@current_user_expenses, @current_user_expenses_of_both, @partner_expenses_of_both)
  @category_badgets = current_user.badgets
end

# shift_months_controller.rb
def past_and_future(cnum)
  @current_user_expenses = ShiftMonth.ones_expenses(current_user, cnum)
  @current_user_expenses_of_both = ShiftMonth.ones_expenses_of_both(current_user, cnum)
  @partner_expenses_of_both = ShiftMonth.ones_expenses_of_both(partner, cnum)
  @sum = @current_user_expenses.sum(:amount)
  @both_sum = ShiftMonth.must_pay_one_month(current_user, partner, cnum)
  @category_sums = Expense.category_sums(@current_user_expenses, @current_user_expenses_of_both, @partner_expenses_of_both)
  @category_badgets = current_user.badgets
end 

この二つのコントローラは全く同じ、ビューにレンダリングされます。

これは後々のことを考えると、何か修正したりするときにかなり手間がかかります。

ビューはインスタンス変数に依存するんです。 ビューのインスタンス変数の数だけコントローラで定義しなければいけないんです。 コントローラは薄くというのは鉄板らしいですね。

コントローラを薄くするには

これまでは単純にモデルにクラスメソッドとして計算式を書いて、それをコントローラで呼び出してインスタンス変数に入れてビューに渡していました。ビューではその変数をただ表示するだけでした。
これでは修正がかなり大変になります。
そこでコントローラでは必要なレコードをまとめて一気にとりだし、必要な計算や表示順はモデルやヘルパーに定義し、ビューから呼び出すようにしました。
例えば、上のものはこうなります。

#expenses_controller.rb
 def index
  @cnum = 0
  @current_user_expenses = current_user.expenses.this_month
  @partner_expenses = partner.expenses.this_month.both_t
end

# shift_months_controller.rb
def past_and_future(cnum)
  @cnum = params[:id].to_i
  @current_user_expenses = ShiftMonth.ones_expenses(current_user, @cnum)
  @partner_expenses = ShiftMonth.ones_expenses(partner, @cnum).both_t
end 

たったこれだけになります。前者ではインスタンス変数をわけていたものを、後者ではまとめています。
必要な計算やクエリなどはモデルとヘルパーに書いていきます。

モデルか?ヘルパーか?

ビューにはできるだけロジックを書かないというのが鉄板です。
viewの役割であるブラウザで何を表示するかというのがわかりにくくなりますもんね。
そこでビューに関するロジックは大体はモデルかヘルパーに書いていきます。
メソッドを定義するときに重要な点は、selfメソッドが使えるかどうかです。
このselfというのは定義するところによって、インスタンス自身かクラス自身かが違ってきます。
selfが使えなさそうなら、ヘルパーに書きましょう。
使えるなら、モデルに定義しましょう。
上記の@current_user_expensesと@partner_expensesを例にしてみます。
@current_user_expensesは自分が払った全ての出費を呼び出しています。全ての出費というのは、
・自分のためだけの出費
・自分とパートナー二人のための出費
・パートナーのためだけの出費
@partner_expensesはuserが違うだけで@current_user_expensesと定義は同じです。
この両方を使って自分の支出合計値を計算したいとします。
この場合、モデルにこの両方を引数で渡して、呼びだすことができます。

# expense.rb
def self.one_total_expenditures(current_user_expenses, partner_expenses)
  current_user_expenses.both_f.sum(:amount) + current_user_expenses.both_t.sum(:mypay) + partner_expenses.sum(:partnerpay)
end

# view.html.haml
= Expense.one_total_expenditures(@current_user_expenses, @partner_expenses)

しかし、ヘルパーに書くとモデル名を省くことができます。

# expenses_helper.rb
def one_total_expenditures(current_user_expenses, partner_expenses)
  current_user_expenses.both_f.sum(:amount) + current_user_expenses.both_t.sum(:mypay) + partner_expenses.sum(:partnerpay)
end

# view.html.haml
= one_total_expenditures(@current_user_expenses, @partner_expenses)

このメソッドを他のモデルから呼び出したいとなると話は違いますが、
ビューでモデル名を書くのは嫌だと言う人もいますし、ただビューで計算したいという場合はヘルパーで十分です。

インスタンスメソッドかクラスメソッドか

これはオブジェクト指向に関することです。
一つのレコード(インスタンス)に対してのメソッドは、モデルにインスタンスメソッドとして書きましょう。
例えば、出費テーブル、カテゴリテーブル、そのカテゴリに対する予算テーブルがあるとします。
一つの出費のカテゴリーの予算を表示したいとすると、 アソシエーションを組んであると、 こういう風に書けます。

# view.html.haml
= expense.category.badget.amount

と言う風にも呼び出せますが、何回も書くと長いですよね。
これをインスタンスメソッドとして定義します。

# expense.rb
def category_badget
  self..category.badget.amount
end

# view.html.haml
- @current_user_expenses.each do |expense|
  = expense.category_badget

こうすることですっきりしますね。

複数のレコードに対してはクラスメソッドで定義します。
例えば、ある条件で表示する順番を入れ替えたいとします。

# view.html.haml
= @current_user_expenses.arrange(true).each do |expnese|

# expense.rb
def self.arrange(both_flg)
  expenses = both_flg ? self.both_t : self.both_f
  (並び替えの処理)
end

とすると、すっきりします。 これをヘルパーに書いてしまうと@current_user_expensesとboth_flgを引数で渡さなくはいけません。

まとめ

・ビューで扱うインスタンス変数は要注意
・ビューで呼び出したいメソッドを定義するときは、selfメソッドが使えるかどうかでモデルとヘルパーのどちらに書くか判断する。

complete i guess
refactored expense model and expense helper