Effective Ruby: Struct便利だよ

項目10 構造化データの表現にはHashではなくStructを使おう

CSVファイルの取り込み時などに、それ用のクラスを作るの面倒だなという場合は、Structが良いというお話。 私もこの手のやつはHashだと扱いにくいので、自然とStructを使うようにしていました。
 
でも最近、Nestされた構造化データを扱うときにはHashが便利になるケースもあると最近学びました。
話がそれるので、この話はまたの機会に。
 
以下、本題。

Effective Ruby

Effective Ruby

Hashの問題点

CSVファイルの読み込み時などにHashを使ってしまうと以下のような問題が起きます。

1. keyを意識しないといけなくなる

要はインターフェースとして定義されていないので、利用する際に一々定義されている箇所を確認しないといけなくなります。
 

2. Hasnなのでゲッターメソッドでアクセスできない

そのままですが、他にもtypo時にNoMethodErrorが発生しない。
 

3. 各Hash間を操作するようなメソッドが作れない

例えば、各行に月間の気温統計のCSVがあるとして、そのデータの平均気温も知りたい場合、 各月の平均気温も知らないといけないが、それを定義する場所がない。

require('csv')
class AnualWeather
  def initialize(file_name)
    @readings = []

    # Hashのkeyを意識しないといけない
    CSV.foreach(file_name, headers: true) do |row|
      @readings << {
        date: Date.parse(row[2]),
        high: row[10].to_f,
        low: row[11].to_f,
      }
    end
  end

  # 平均気温
  def mean
    return 0.0 if @readings.size.zero?

    total = @readings.reduce(0.0) do |sum, reading|
      # 各月の平均気温を求めるロジック
      # 本当はここだけ抽象化したい。
      # あとgetter method使えない。
      sum + (reading[:high] + reading[:low]) / 2.0
    end
    total / @readings.size.to_f
  end
end

Structを使うと

最初の2つの問題が解消されます。

class AnnualWeather
  # 項目定義が分離されて見やすい
  Reading = Struct.new(:date, :high, :low)

  def initialize
    @readings = []

    CSV.foreach(file_name, headers: true) do |row|
      @readings << Readings.new(
        Date.parse(row[2]),
        row[10].to_f,
        row[11].to_f)
    end
  end

  def mean
    return 0.0 if @readings.size.zero?

    total = @readings.reduce(0.0) do |sum, reading|
      # getter methodが使える
      sum + (reading.high + reading.low) / 2.0
    end
    total / @readings.size.to_f
  end
end

さらにblockを渡すことで

3つ目の問題が解消されます。

Reading = Struct.new(:date, :high, :low) do
  def initizlize
    super(date, high, low * 1.1)
  end

  # ここに定義できる。わかりやすい。
  def mean
    (high + low) / 2.0
  end
end

覚えておくべき事項

  • 新しいクラスを作るほどでもない構造化データを扱うときには、HashではなくStrutを使うようにしよう
  • Struct::newの戻り値を定数に代入し、その定数をクラスのように扱おう

 

余談

私はどっちかというと上記のblockを渡す方法より、以下のようにclassにしてしまって使うことの方が多かったです。
どっちでもいい気がしますが、以下の場合initializeを書くことができます。

  class Reading < Struct.new(:date, :high, :low)
    def initialize(date, high, low)
      # do something
      super(date, high, low * 1.1)
    end
  end