Struct and OpenStruct in Ruby

The Ruby’s Struct and OpenStruct classes are very useful for some tasks. They act like a simple data structure, similar to hash, but they also allow to access their attributes by calling them directly from the object and not only using the brackets.

Struct

Let’s see and example with Struct.

Computer = Struct.new :ram, :hard_disk
computer = Computer.new
computer[:ram] = "4 MB"
computer.hard_disk = "500 GB"

# Access data with []
computer[:ram]       # => "4 MB"
computer[:hard_disk] # => "500 GB"

# Access data as attributes
computer.ram       # => "4 MB"
computer.hard_disk # => "500 GB"

In line 1, we defined a struct indicating the attributes it will contain. This instruction is very interesting: in theory, it should return a Struct object, but it doesn’t. Actually, it returns a Class object, the same type of object that Ruby generates when a class is defined (yes, in Ruby, the class definition is also an object, but we will not go into details for now). This object can be assigned to a variable (or a constant, like in this case), and it is also possible to generate objects from it, as we do in line 2.

In line 3, we assign a value to one of the attributes using the hash syntax, and in the line 4 we do the same with the other attribute but using the method syntax. The lines 7 and 8, and the lines 11 and 12 show that the data can be accessed in both ways too.

Also, Struct allows to define methods as part of the structure. Let’s see:

Computer = Struct.new(:ram, :hard_disk) do
  def descripcion
    "Computer with #{ram} ram and #{hard_disk} hard disk."
  end
end

computer = Computer.new("4 MB", "500 GB")
computer.descripcion
# => Computer with 4 MB ram and 500 GB hard disk.

In this example we can see how a method is defined in a Struct and also we see that it is possible to create the object by indicating the values it will contain, like if we defined an initialize method that receives these parameters. The order of the values must match the order of the parameters defined for the structure (see lines 1 and 7).

The params of Struct are not strict. If we declare that it receives 3 params and when creating we only send 1, the remaining 2 are initialized with nil. On the other hand, if we send more than 3 params, we get an exception.

computer = Struct.new(:ram, :hard_disk, :processor).new("4 MB")
# => #<struct ram="4 MB", hard_disk=nil, processor=nil>

computer = Struct.new(:ram).new("4 MB", "500 GB", "2.5 GHz", "1024x768")
# => ArgumentError: struct size differs

In the previous examples we can also see that it is possible to create Struct’s objects in a line.

As we can see, it is a very versatile class that allows us to create objects with certain behavior with no need of creating a class for that.

OpenStruct

OpenStruct behaves similarly, but it allows us to add attributes on the fly, ie with no need to define them previously. Let’s see the following example:

require 'ostruct'

computer = OpenStruct.new ram: '4 MB', hard_disk: '500 GB'
computer.ram # => '4 MB'
computer[:hard_disk] # => "500 GB"

# attributes can be created 'on the fly'
computer.screen = "1024x768"
computer.screen   # => "1024x768"
computer[:screen] # => "1024x768"

The first thing to note is that now we need to include the ostruct library because it is not part of the core but of stlib; this is done in line 1. In line 3 an OpenStruct object is created and initialized at the same time. Unlike Struct, there is no intermediate class, but the created object directly. To initialize it we can assign a hash directly, like in the example. The lines 4 and 5 verify the values can be accessed the same way they can be accessd with Struct.

In line 8 we assign a value to a no-defined attribute. We didn’t indicate our structure to accept values for screen, but OpenStruct knows how to handle these cases by creating this attribute on the fly. In lines 9 and 10 we see it is possible to access it with no problem.

Another important difference is that OpenStruct does not allow to define methods in its declaration, as Struct does.

Conclusions

The classes Struct and OpenStruct provide a simple way to create data structures with the behavior of a class. This is particulary useful when refactoring and we are not sure about what new classes we need to create, then we can use Struct or OpenStruct as an intermediate step. Obviously, if the structure becomes more complex and requires more effort to be mantained, it is time to move it to a real class.

OpenStruct, besides not being part of the core, is slower than Struct, thus we must prefer Struct whenever possible and use OpenStruct only when necessary.

Here are the docs for Struct and OpenStruct.

comments powered by Disqus