misterinevitable

Structs and OpenStructs

Sometimes you have a set of related data that you want to travel together, e.g. as Data Transfer Objects (DTOs). In these cases, a class might not be necessary, and perhaps inconvenient if you want to be able to compare these objects using value-semantics, since classes by default compare references.

You could use Hashes to side-step this, but they can be inconvenient for another reason: accessing the members is syntactically different than invoking methods. If you want to support method-call syntax AND use value-semantics, you can use a Struct or OpenStruct.

Struct

Food = Struct.new(:name, :price, :has_gluten)

menu = [
  Food.new("Spaghetti Simpatico", "$12.00", true),
  Food.new("Beef Tonight", "$19.00", false),
  Food.new("Pizza ala Cauliflower", "$15.00", false)
]

# output gluten-free menu
puts menu.reject(&:has_gluten)

You can use keyword arguments instead if you define the Struct with keyword_init set to true.

Food = Struct.new(:name, :price, :has_gluten, keyword_init: true)

menu = [
  Food.new(name: "Spaghetti Simpatico", price: "$12.00", has_gluten: true),
  Food.new(name: "Beef Tonight", price: "$19.00", has_gluten: false),
  Food.new(name: "Pizza ala Cauliflower", price: "$15.00", has_gluten: false)
]

# output gluten-free menu
puts menu.reject(&:has_gluten)

If you want to define other methods in the Struct, you can do that, too.

Food = Struct.new(:name, :price, :has_gluten, keyword_init: true) do
  alias :gluten? :has_gluten

  def menu_entry
    "#{name} ... #{price}#{gluten? ? '' : ' (GF)'}"
  end
end

menu = [
  Food.new(name: "Spaghetti Simpatico", price: "$12.00", has_gluten: true),
  Food.new(name: "Beef Tonight", price: "$19.00", has_gluten: false),
  Food.new(name: "Pizza ala Cauliflower", price: "$15.00", has_gluten: false)
]

# output list of formatted menu items
puts menu.map(&:menu_entry)

Here, I used alias and a regular def to define methods. It’s good to ask yourself if you actually need a class if you find yourself defining methods on a Struct. Using alias is probably OK, but adding actual behavior might mean you are using the object as more than a DTO.

OpenStruct

You might have a Hash object which you want to access using method-call syntax. One way you could do this is with a Struct.

hash = { name: 'Spaghetti Simpatico', price: '$12.00', has_gluten: true }

# define the struct
Food = Struct.new(*hash.keys, keyword_init: true)

# create the instance
object = Food.new(**hash)

# use the instance
puts object.name

This is cumbersome, though, since you have to define the struct, then create the instance. An easier method is to use OpenStruct.

require 'ostruct'

hash = { name: 'Spaghetti Simpatico', price: '$12.00', has_gluten: true }

# define the struct
object = OpenStruct.new(hash)

# use the instance
puts object.name

It’s not as convenient to define methods on these since each call creates a new class. If you just have a single instance you are working with you can define a singleton method on the instance.

If, however, you have several instances, you can subclass OpenStruct and define the method there, but if you’re defining a class for this reason, you might as well not use OpenStruct at all and just define a class or use a regular Struct.

When using OpenStruct, be sure to read through the caveats.

Lastly, see the mindmap at the end of the RubyGuide on using Struct and OpenStruct. It’s helpful to see the differences between classes vs Structs vs OpenStructs and can help you figure out which one to use for your use case.