Rubyでクラスを作っている時に、それを比較演算子で比較したい、あるいはソートしたいなどのニーズが出てくることがあります。そんな時に使えるのがComparable
です(※ソートは宇宙船演算子さえあればできます)
class MyClass
include Comparable
def <=>(other)
# put your code here...
end
end
こうすることで、Comparable
モジュールで提供する機能を利用できるようになりました!詳しい説明は公式docに譲ります。
問題点
さて、こうすることで比較が可能になりましたが、例えば以下のような場合を考えてみましょう。
class Human
include Comparable
attr_reader :income, :height
def initialize(income, height)
@income = income # 年収
@height = height # 身長
end
def <=>(other)
@income <=> other.income
end
end
年収で人間を比べるんじゃない! というのは置いといて、人間がある一つの軸で比較可能であるという考え方は、素朴すぎるとは思いませんか?
実際のシステム開発においても、比較のロジックは欲しいけど、対象のクラスを比較する指標はいくつか考えられるし、とは言え毎度sort_by
でブロックの中身を書くのもだるい...ということはよくあります。
解決策
クラス自身をComparableにするのではなく、Comparableなフィールドに移譲しましょう
婚活女子の山本さんは、男は年収が6割、身長が4割であると主張しています。彼女の考えを数値化した山本指数が婚活界隈で有名になり、流行に敏感なあなたは自分が開発するシステムにこれを取り込もうと考えました。簡単に実装すると以下の通りです。
class Human
# 中略
def yamamoto_index
@income * 0.6 + @height * 0.4
end
end
# Humanの配列を山本指数でソートする
humans.sort_by(&:yamamoto_index)
書いてみれば特に難しいことはしていませんが、他の指標を追加する時は同じようにメソッドを実装するだけで済みますし、比較する際の指標も明示的でいい感じですね。
比較ロジックの拡張性を高める
筆者は、場合によっては上記のコードではまだ不完全であると考えています。
この実装では、比較ロジックを変更したい場合に手を加えられるのはHumanクラスです。しかし、この場合変わったのはHumanの振る舞いではなく評価指標です。であれば、変更を「評価指標クラス」に閉じたいのがプログラマの情ってもんですよね?
また、指標の実装が複雑な計算を伴うようになったとして、Humanクラスが肥大化していくのは避けたいです。
class YamamotoInex
include Comparable
def initialize(income, height)
@income = income # 年収
@height = height # 身長
end
def <=>(other)
comparator <=> other.comparator
end
def comparator
@income * 0.6 + @height * 0.4
end
end
このクラスをHumanクラスのyamamoto_indexメソッドで初期化して返すようにしてあげましょう。
これで山本指標が見直された場合でも、Humanクラスに変更を施す必要がなくなりました!