最近ruby標準のOpenStruct
ライブラリが便利だなと思ったので、使い方とかメモ✍
OpenStructとは?
OpenStruct
はrubyの標準ライブラリです。マニュアルには下記のように記載されてます。
要素を動的に追加・削除できる手軽な構造体を提供するクラスです。 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