DEV Community

Loran Kloeze
Loran Kloeze

Posted on • Updated on

Alternatives for Ruby's OpenStruct

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)

The discussion has been locked. New comments can't be added.