手続き的で冗長な Ruby のコードを Squeak/Pharo Smalltalk の類似機能を活用してよりシンプルに書き換える

こんな感じの“イケてない”と称されるコードを改善する話。

  def total_sales_within_date_range
orders_within_range = []
@orders.each do |order|
if order.placed_at >= @start_date && order.placed_at <= @end_date
orders_within_range << order
end
end

sum = 0
orders_within_range.each do |order|
sum += order.amount
end
sum
end

Rubyのリファクタリングでイケてないコードを美しいオブジェクト指向設計のコードへ改良するための方法 - その1 - ベルリンのITスタートアップで働くジャバ・ザ・ハットリの日記


元記事では、Smalltalk 由来のいわゆる「〜ect系」メソッドの導入によりコードをシンプルに書き換えていますが、もうちょっと Ruby や Rails に備わっている機能を使うことはできないのかなぁ、とリファレンスを紐解きながらこんなふうにしてみました。

  def total_sales_within_date_range
    within_date_range = ->order{ order.placed_at.between?(@start_date, @end_date) }
    @orders.select(&within_date_range).sum(&:amount)
  end


範囲に収まっているかどうかの判定は無名関数(Proc)にして名前を付け、select に渡しています。Ruby の無名関数は Smalltalk のと違い、〜ect系メソッドの引数としてはそのまま渡せないので、& を付ける必要があります。

範囲に収まっているかどうかの判定処理記述の中身についても、Date が Numeric 同様 Comparable なのを利用して簡潔な between? に置き換えています。Smalltalk にも Magnitude>>#between:and: がありますね。


map(&:amount).inject(0, :+) も冗長で意図が伝わりにくいので sum ひとつに置き換えました。ただ、ここで使った sum は Smalltalk の sum とは違って、次のような定義を想定しています。Rails や Ruby2.4 の sum はよく知らないので、こういう動きでなかったらごめんなさい。

class Array
  def sum(zero = 0, &b)
    inject(zero){ | s, e | s + (b ? b[e] : e) }
  end
end


Ruby の制約として残念だったのは、Proc の within_date_range をクエスチョンマークを使って within_date_range? としたかったのが許されなかったところ。メソッド名にすればクエスチョンマークもOKなのですが、そうすると今度は select の引数にするときに記述が面倒になるので痛し痒しですね。