Alternatives for Ruby’s OpenStruct

Loran Kloeze
Towards Dev
Published in
3 min readJul 10, 2022

--

This week I was updating Rubocop in an older Rails application. And that means Rubocop gets to throw new rules in my face. One of those new rules was about me using OpenStruct which is apparently now considered an anti-pattern. Time for an alternative for OpenStruct.

Using OpenStruct is a quick way to create some kind of hash where the keys are accessible through methods. But OpenStruct has a huge drawback: there are concerns regarding performance according to this and this source. Rubocop says:

Instantiation of an OpenStruct invalidates Ruby global method cache as it causes dynamic method definition during program runtime. This could have an effect on performance, especially in case of single-threaded applications with multiple OpenStruct instantiations.

So if using OpenStruct is most of the time an anti-pattern, what are some good alternatives for OpenStruct and how do they benchmark against it in Ruby 3.1.2?

First of all: there are cases where using OpenStruct is valid. Creating some kind of Ruby script that runs once? Just use OpenStruct at your own discretion, run it and throw the script away. Using OpenStruct in a Rails application where it is used very often? Don't do that and check out the alternatives below. My point is: don't just let some random guy like me on the internet tell you what you should do in each and every case.

OpenStruct

Let’s start with a quick refresher on OpenStruct.

require 'ostruct'car = OpenStruct.new
car.wheels = 4
car.mileage = 13_337
puts "My car has {car.wheels} wheels and"
puts "a mileage of #{car.mileage} miles"
# => My car has 4 wheels and
# => a mileage of 13337 miles

Wonderful. The syntax is quite clean, dynamic and easy to work with. Now let’s do it 250,000 times.

require 'benchmark'
require 'ostruct'
cars = []
time = Benchmark.measure do
250_000.times do
car = OpenStruct.new
car.wheel = 4
car.mileage = rand((1..100_000))
cars << car
end
end
puts time
# => 6.662331 0.400225 7.062556 ( 7.062667)

That took 7.06 seconds. Let’s continue with some alternatives.

Class

The good old class. Always there in Ruby when we need it. When we use a customer class as an alternative for the above OpenStruct it looks like this:

class Car
attr_accessor :wheels, :mileage
end
cars = []
time = Benchmark.measure do
250_000.times do
car = Car.new
car.wheels = 4
car.mileage = rand((1..100_000))
cars << car
end
end
puts time
# => 0.091187 0.000000 0.091187 ( 0.101195)

This time it took 0.10 seconds. That’s 70 times faster than using OpenStruct.

Using a class is a good alternative. It requires some more code but that's fine. And in this case, using a class conveys nice what this piece of your business domain looks like.

Struct

Of course we should look at Struct too:

car_struct = Struct.new(:wheels, :mileage)
cars = []
time = Benchmark.measure do
250_000.times do
cars << car_struct.new(4, rand((1..100_000)))
end
end
puts time
# => 0.085577 0.000649 0.086226 ( 0.086235)

With Struct it took 0.09 seconds, similar to using a class.

Hash

The last alternative is a plain hash:

cars = []
time = Benchmark.measure do
250_000.times do
cars << { wheels: 4, mileage: rand((1..100_000)) }
end
end
puts time
# => 0.085295 0.000000 0.085295 ( 0.085301)

And hash is also similar to using a class or Struct with a run time of 0.09 seconds

Benchmark

To summarize:

OpenStruct   - 6.66 0.40  7.06 (7.06) - baseline
Custom class - 0.09 0.00 0.09 (0.10) - ~70 times faster
Struct - 0.08 0.00 0.08 (0.08) - ~80 times faster
Hash - 0.08 0.00 0.08 (0.08) - ~80 times faster

What should I choose?

Any of the above alternatives of OpenStruct is fine. If you want to squeeze out the last few percentages of performance, go with a Struct or hash. If you only use the data at a certain point in your code, a hash is also appropriate. But as soon as that data flows through multiple paths in your code, don't be afraid to upgrade the hash to a custom class or Struct . A class will convey better what your data model looks like and can eventually enforce it as well so it is more resilient. You cannot really make a wrong choice here and you can always upgrade the data structure at a later moment, Ruby is nice that way.

--

--

I’m a Dutch software engineer with 15+ years experience and work mostly with Ruby (on Rails) / Javascript / React.