Rubyで、複数の値を一つの引数として受け取りたい場合、Hashではなく独自クラスかStructを使うように設計しよう

最近の自分のテーマのひとつとして、「複雑で巨大になっていくシステムをRubyそしてRuby on Railsで上手く作っていくか」というのを考えています。

最終的には上手く整理してまとめたいのですが、一旦考えている小さなアイディア一つ一つをブログしていく予定で、今回は「Hashの代わりに独自クラスかStructを使う」というアイディアについて書いていきます。

RubyにはHashというkey -> valueの形式で複数の値を保持できるクラスがあります。 これはRubyでプログラミングするときに使う基本的なデータ型でもあり、非常に便利なので複数の値を保持するような構造を持つデータを表現するためによく使われます。

例えば四角形(Square)の面積を計算するプログラムを書きたい場合、縦(height)と横(width)の長さの値が必要なために、以下のようにHashを引数として受け取るプログラムを書くことがあります。

# 四角形の面積を計算する
def calc_area(square)
  square[:height] * square[:width]
end

square = { height: 200, width: 100 }

calc_area(square)

では、このプログラムの何が問題なのでしょうか?

引数が特定のキーを持つハッシュであることを想定しているメソッドにtypoなどをして間違ったキーを持つハッシュを渡した場合以下のようなエラーが発生します。

invalid_square = { height: 200, :widthh: 100}
calc_area(square)
#=> TypeError (nil can't be coerced into Integer)

これは「nil は Integer に型変換できない」ということを意味しています。(または「nilはIntegerとして処理することができない」)

これだけだと、そのエラーメッセージだけで引数に与えている値が間違っていることを推測することは難しくメソッド内部の実装を読んでいかなければなりません。

calc_areaを、たとえばheightとwidthというメソッドを持つオブジェクトを受け取るメソッドとして実装すると間違った引数を与えた場合のエラーメッセージが変わります。

# 四角形の面積を計算する
def calc_area(square)
  square.height * square.width
end


Square = Struct.new(:height, :width)

valid_square = Square.new(200, 100)
calc_area(valid_square)
#=> 20000

invalid_square = Struct.new(:height, :widthh).new(200,100)
calc_area(invalid_square)
#=> NoMethodError (undefined method `width' for #<struct height=200, widthh=100>)

このように、引数で渡したオブジェクトにwidthというメソッドがないということが明確に指摘されます。 このエラーメッセージのわかりやすさの違いは小さなプログラムでは大した違いになりませんが、システムが大きく複雑になるほどに適切なエラーメッセージがでることは生産性に大きく影響が出るために非常に重要です

引数をハッシュではなく特定のメソッドをもつオブジェクトとして受け取るメリットは設計の改善をしていく上での柔軟性を得るためにも重要です。

例えば四角形(Square)を表すデータ構造が様々なところで使う事がわかり、独自クラスとして定義し直す場合でもcalc_areaは実装を変更することなく利用することができます。

class Square
  attr_accessor :height, :width
end

s = Square.new(200, 100)
calc_area(s)
#=> 20000

このように設計を改善していく上でも、メソッドの引数としてHashではなく 特定のメソッドをもつオブジェクトを受け取るようにすることは重要です。

では、なぜ多くの人がHashを受け取るようなコードを書いてしまうのでしょうか?

これはおそらく、自分たちが普段利用するライブラリやフレームワークのコードが複数の値を引数で受け取りたいときにHashを使っているからだと考えられます。

では、なぜライブラリやフレームワークではHashを受け取るようにしているのでしょうか?

これは複数の値を一つのオブジェクトとしてまとめて受け渡す方法としてHashがもっとも簡単な方法ではあるからです。Hashはシンタックスシュガーも用意されているため、わざわざはHash.newなどしなくても{}を使うことでオブジェクトを作ることができます。また特定のアプリケーションドメインによらないデータ形式なので、大規模プログラムでも書き捨てのプログラムでも使うことができます。

ライブラリのコードは様々なユースケースで簡単に使えるようにRubyの基本的なデータ構造(String, Integer, Hash, Array)を受け取る方にしたほうがよく、逆に私たちが作るシステムのプログラムは呼び出しを多少を面倒にする代わりに表現力、柔軟性を増してメンテナンスしやすいように特定のメソッドを持つオブジェクトを受け取るようにしたほうが良いと考えています。

まとめ

アプリケーションコードで複数の値を一つの引数として受け取りたい場合には、Hashではなく特定のメソッドをもつオブジェクト、独自クラスまたはStructを受け取るようなコードを書こう。