Alternatives for Ruby's OpenStruct
Tags: ruby, rails
Published: 2022-07-10 16:47:45

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](https://www.honeybadger.io/blog/how-openstruct-and-hashes-can-kill-performance/#:~:text=Performance%20Implications) and [this](https://stackoverflow.com/a/14421228) source. Rubocop [says](https://www.rubydoc.info/gems/rubocop/0.61.1/RuboCop/Cop/Performance/OpenStruct):

> 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?

<small>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.</small>

## OpenStruct

Let's start with a quick refresher on `OpenStruct`. 

```ruby
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.

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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.