Rubyのsendとinjectを使って短くスッキリしたコードを書く

こんにちは!kossyです!



1ヶ月ほど更新できていませんでした。。。
大きめの機能追加に目処がついたため、少し余裕ができました。
これから可能な限り、ブログを更新したいと思います。




さて、今回はRubyのsendメソッドとinjectメソッドを使って、短いコードを書く方法をブログに残してみたいと思います。




環境
Ruby 2.5.1
Rails 5.2.3
MacOS Mojave




sendメソッドとinjectメソッド

まず、両メソッドについて説明します。

と言っても、公式リファレンスのリンクを貼るだけですが、、、

sendメソッド
https://docs.ruby-lang.org/ja/latest/method/Object/i/__send__.html

オブジェクトのメソッド name を args を引数にして呼び出し、メソッドの実行結果を返します。

ブロック付きで呼ばれたときはブロックもそのまま引き渡します。

send が再定義された場合に備えて別名 __send__ も用意されており、ライブラリではこちらを使うべきです。また __send__ は再定義すべきではありません。

send, __send__ は、メソッドの呼び出し制限にかかわらず任意のメソッドを呼び出せます。 クラス/メソッドの定義/呼び出し制限 も参照してください。

引用: https://docs.ruby-lang.org/ja/latest/method/Object/i/__send__.html

メソッドを動的に実行したい時なんかに使ったりしますね。
「黒魔術」と揶揄されたりもしてます。https://qiita.com/Yuki_Nakagami/items/21319c10e833cd0efc12

今回のブログでは、ゴリゴリ使います。


injectメソッド
https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/inject.html

リストのたたみこみ演算を行います。

最初に初期値 init と self の最初の要素を引数にブロックを実行します。 2 回目以降のループでは、前のブロックの実行結果と self の次の要素を引数に順次ブロックを実行します。そうして最後の要素まで繰り返し、最後のブロックの実行結果を返します。

要素が存在しない場合は init を返します。

初期値 init を省略した場合は、最初に先頭の要素と 2 番目の要素をブロックに渡します。また要素が 1 つしかなければブロックを実行せずに最初の要素を返します。要素がなければブロックを実行せずに nil を返します。

引用: https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/inject.html

金額の合計値とかを出したい時によく使ったりします。
当たり前ですが、配列やハッシュをカスタマイズして新しい値を返したい時がユースケースとしては一番多いですね。


前提

家計簿アプリ的な物を例にしてみます。

・Accountsテーブル(家計簿そのもの)
・コストごとにテーブルが分かれているとします。(今回は「食費」「光熱費」「交際費」のみとします。)



食費モデル(MealCost)
光熱費モデル(UtilityCost)
交際費モデル(EntertainmentCost)

は、

家計簿モデル(Account)に属し(belongs_to)、

家計簿モデル(Account)はそれぞれのコストをhas_manyする関連があるとします。

それぞれのコストは、dateとpriceとnameをattrとし持っているとします。

前提は以上です。


メソッド定義

その月のコスト全てを合計した総額を返すようなメソッドを定義してみます。

普通に考えると、

class Account
  def total_price(target_month)
    meal_costs.where('? <= date && date < ?', target_month.to_time.beginning_of_month, target_month.to_time.end_of_month).inject(0){|sum, meal| sum += meal.price } + utility_costs.where('? <= date && date < ?', target_month.to_time.beginning_of_month, target_month.to_time.end_of_month).inject(0){|sum, utility| sum += utility.price } + entertainment_costs.where('? <= date && date < ?', target_month.to_time.beginning_of_month, target_month.to_time.end_of_month).inject(0){|sum, entertainment| sum += entertainment.price }
  end
end

みたいなメソッドを定義することになるかなぁ、、、(異論は認めます)


でも、コストは上記の3種類以外にもありますし、コスト項目が増えると見通しも悪くなります。

そこで、sendを駆使してみます。

class Account
  ITEM_COSTS = [
    'meal',
    'utility',
    'entertainment'
  ]

  def total_price(target_month)
    ITEM_COSTS.inject(0) do |total, item|
      total += self.send("#{item}_costs").where('? <= date && date < ?', target_month.to_time.beginning_of_month, target_month.to_time.end_of_month).inject(0){|sum, _item| sum += _item.price }
    end
  end
end

中身がコスト名の定数ITEM_COSTSを定義し、total_priceメソッド内でsendメソッドとinjectメソッドを使って合計額を算出する形に切り替えました。

これなら、コスト項目が増えてもITEM_COSTSに足すだけでいいので、変更にも強いかなと思います。