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 multipleOpenStruct
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.
Top comments (0)