OpenStruct in Ruby
In this article, we’re going to explore the following topics:
- the
OpenStruct
class - data structure: creation and manipulation
OpenStruct
and lazy loading- data structures behind the scene
Feel free to read the
Struct in Ruby
article if you are unfamiliar with data structures in Ruby. This will give you a better comprehension of the technical notions of this article.
The OpenStruct class
OpenStruct
allows the creation of flexible data structures. Indeed, the structure doesn’t need to provide a rigid list of members as it doesn’t define any structure type but data structures that you fill in afterward. It provides a pair of getter/setter methods for each attribute. This is similar to attr_accessor
method for classes.
Feel free to read
Attributes in Ruby
if you are unfamiliar with theattr_*
methods in Ruby.
This class is part of Ruby’s Standard Library (a.k.a stdlib
). Let’s have a look to OpenStruct
ancestor chain
irb> require 'ostruct'
=> true
irb> OpenStruct.ancestors
=> [OpenStruct, Object, Kernel, BasicObject]
As it’s not part of the Ruby Core, we have to require
the ostruct
library which contains the OpenStruct
class definition. OpenStruct
inherits from Object
— which is the default parent class in Ruby. Note that it doesn’t include any particular module. Indeed, unlike the Struct
class, it doesn’t include the Enumerable
module.
Feel free to read the
Ruby Object Model
article if you are unfamiliar with theObject
class and the ancestor chain.Feel free to read the
The Enumerable module in Ruby: Part I
article if you are unfamiliar with theEnumerable
module.
Data structure: creation and manipulation
The OpenStruct::new
is in charge of creating a new data structure. This method accepts a Hash
, a Struct
or an OpenStruct
as parameter
Firstly, we instantiate a new OpenStruct
object with one attribute called ram
and we store this object in the computer
variable. Then we access ram
attribute in 3 different ways:
computer.ram
: theram
accessor methodcomputer[:ram]
:OpenStruct#[]
method with asymbol
keycomputer['ram']
:OpenStruct#[]
method with astring
key
Next, we define the screens
attribute. There are 3 ways to define a new attribute or to modify the value of an existing one:
computer.screens=
: thescreens=
accessor methodcomputer[:screens]=
:OpenStruct#[]=
method with asymbol
keycomputer['screens']=
:OpenStruct#[]=
method with astring
key
Finally, we access the value of the screens
attribute the same way as the ram
attribute. Unlike Struct
that defines a structure type with a rigid attributes list, the OpenStruct
class defines a new data structure that can add attributes on the fly — like the attributescreen
in the above example.
OpenStruct
and lazy loading
To save memory and speed up the access to an attribute, the accessor methods of an attribute are lazy loaded at certain points. This means that the methods are defined only when a set of defined actions are triggered. This allows your program to only define the minimum amount of required methods to make your data structure work
After the OpenStruct
initialization, we see that computer.ram
and computer.ram=
accessor methods are not defined yet.
Indeed, we can see that after a call to computer.ram
, computer.ram
and computer.ram=
methods are now defined.
Note that a call to
computer[:ram]
doesn’t define the accessor methods.
Then we set a value to the :screens
attribute via the OpenStruct#[]=
method. After this method call, the computer.screens
and computer.screens=
accessors are defined. To recap, accessor methods are defined only when:
- an accessor method of an existing (or not) attribute is called for the first time.
OpenStruct#[]=
is called for the first time —computer[:screens] = 2
for example.
Data structures behind the scene
Now that we are more familiar with the OpenStruct
class, let’s dive into what happens behind the scenes when we create and manipulate an OpenStruct
object. Each instance ofOpenStruct
defines an internal hash that contains a mapping table for each attribute and value. This container is commonly called the table
Note that each attribute name is converted to a Symbol
before to be included into the table
. Now let’s have a look at the evolution of the computer
‘s table
Here, the table
of the computer
’s data structure is { ram: "4GB" }
. Now let’s add the screens
attribute to the computer
object
At this point the content of the table is { ram: "4GB", screens: 2 }
. What happens when an attribute that doesn’t exist is called? For example, a call to the computer.cores=
setter method
When a method is not defined within the entire ancestor chain of a given object then Ruby automatically calls the first defined method_missing
Hook Method in this ancestor chain. OpenStruct
defines its own version of method_missing
. So when computer.cores = 2
is called then the OpenStruct#method_missing
method is called behind the scene.
Let’s detail the execution flow of this method when the computer.cores = 2
is called:
- 1/ Getter and setter methods are defined and the key passed as a parameter is converted into a
Symbol
—:cores
- 2/ The value is inserted into the internal hash using this key —
@table[:cores] = 2
- 3/ The value is returned —
2
This execution flow is slightly the same for a call to computer[:cores] = 2
and computer.cores
.
Ruby Mastery
We’re currently finalizing our first online course: Ruby Mastery.
Join the list for an exclusive release alert! 🔔
Also, you can follow us on x.com as we’re very active on this platform. Indeed, we post elaborate code examples every day.
💚