Madogiwa Blog

主に技術系の学習メモに使っていきます。

Ruby: OpenStructが便利な気がしたので使い方とかメモ

最近ruby標準のOpenStructライブラリが便利だなと思ったので、使い方とかメモ✍

OpenStructとは?

OpenStructrubyの標準ライブラリです。マニュアルには下記のように記載されてます。

要素を動的に追加・削除できる手軽な構造体を提供するクラスです。 OpenStruct のインスタンスに対して未定義なメソッド x= を呼ぶと、 OpenStruct クラスの BasicObject#method_missing で捕捉され、そのインスタンスインスタンスメソッド x, x= が定義されます。 この挙動によって要素を動的に変更できる構造体として働きます。 class OpenStruct (Ruby 2.6.0)

要するに要素を動的に追加できる手頃な構造体を表現するクラスです📦
使用例は下記のような感じです👀

require 'ostruct'
# 構造体を作って動的にpropertyを追加
son = OpenStruct.new
son.name = "Thomas"
son.age = 3

# hashでもOK
son = OpenStruct.new({ :name => "Thomas", :age => 3 })
p son.name #=> "Thomas"

メソッドチェインでpropertyにアクセスできるのが便利ポイントかなと思っています🙌

具体的な使用例

Testするときとかに、わざわざ仮のClassとか定義するのはちょっと大変なのでメソッドチェインでアクセスできるMockを作るときとかに便利かなと👀

let(:mock_post) { OpenStruct.new(title: 'title', body: 'body') }
expect(Hoge.call(mock_post)).to eq fuga

あとは下記のようなCSVをパースして、処理のオブジェクトを作り返却後、何かしらの処理を行うようなプログラムがあったとします。items.reject(&:done)をやりたいので、Hashではなくメソッドチェインでアクセスできる何らかのオブジェクトを返却したいです。そういう場合に、下記みたいになattr_readerだけ定義されたClassとか作ってしまうこともあるかと思うのですが、、、

def download(path)
  items = CsvParser.call(path)
  items.reject(&:done).each do
    # なんかしら処理して保存するような処理
  end
end

class CsvParser
  require 'csv'
  
  def self.call(path)
    table = CSV.table(path)
    table.map { |row| Item.new(row) }
  end

  class Item
    def initialize(title:, body:, done:)
      @title = title
      @body = body
      @done = done
    end

    attr_reader :title, :body, :done
  end
end
p download('./date.csv')

OpenStructを使うとかなりスッキリかけます✨またClassのように受け取るCSVの定義が変わってもattr_readerのを変更しなくてもいいところも良いのかなと!(ケースバイケースですが)

def download(path)
  items = CsvParser.call(path)
  items.reject(&:done).each do
    # なんかしら処理して保存するような処理
  end
end

class CsvParser
  require 'csv'

  def self.call(path)
    table = CSV.table(path)
    table.map { |row| OpenStruct.new(row) }
  end
end
p download('./date.csv')

おまけ:パフォーマンスの話

パフォーマンスについても調査したので書いときます。そもそもなんで調査しようと思ったかというとOpenStructを使った方がClass定義するより、パフォーマンスもいいよねって話を書こうと思ったんですが(Classより構造体の方が軽いと思って)、調べたこと無かったので実際にruby標準のBenchmarkライブラリを使ってベンチマークをとってみましたが、Classの方がちょっと(1.15倍ぐらい)効率良かったです😭

👇ベンチマークをとったコードはこちら。100,000回、attr_readerを定義したClassをnewするのとOpenStruct.newを比較しました。(あとオマケに構造体とHashも測ってみた👀)

require 'benchmark'
require 'ostruct'

class Sample
  def initialize(hoge:, fuga:)
    @hoge = hoge
    @fuga = fuga
  end

  attr_reader :hoge, :fuga
end

struct = Struct.new('SampleStruct', :hoge, :fuga)

n = 100_000
Benchmark.bm(10) do |b| b.report(:class) { n.times { Sample.new(hoge: 'hoge', fuga: 'fuga') } } }
Benchmark.bm(10) { |b| b.report(:ostruct) { n.times { OpenStruct.new(hoge: 'hoge', fuga: 'fuga') } } }
Benchmark.bm(10) { |b| b.report(:struct) { n.times { struct.new(hoge: 'hoge', fuga: 'fuga') } } }
Benchmark.bm(10) { |b| b.report(:hash) { n.times { Hash.new(hoge: 'hoge', fuga: 'fuga') } } }

結果がこちら、
🥇Hash: 単純なHashの生成
🥈struct: 構造体の生成
🥉class: atrr_readerだけのclass
😭ostruct: OpenStructの生成

                 user     system      total        real
class        0.054316   0.000390   0.054706 (  0.054914)
                 user     system      total        real
ostruct      0.062140   0.000261   0.062401 (  0.062630)
                 user     system      total        real
struct       0.034693   0.000268   0.034961 (  0.035296)
                 user     system      total        real
hash         0.029182   0.000254   0.029436 (  0.029677)

Classの方が効率がいいのが意外でしたが、よくよく考えるとOpenStructは動的にpropertyを定義できるので、attr_readerだけののクラスの場合だったら、Classの方がパフォーマンスがいいのも頷ける感じですね🤔

おわりに

今回は、OpenStructについて書いてみました。パフォーマンスの件は、若干あれでしたがメソッドチェインでアクセスしたいだけなのにActiveRecordのオブジェクトを使っているところ等があれば、OpenStructを使った方が効率が良さそうです👀

rubyの標準ライブラリは見てみると便利なものがたくさんあるので、読んで見るとよさそうですね🙌 https://docs.ruby-lang.org/ja/latest/library/index.html