In simple words, Ruby Struct is a built-in class which provides useful functionalities and shortcuts. You can use it for both logic and tests. I will quickly go through its features, compare with other similar stuff and show some less-known but still useful information about it.

Basic usage

Employee = Struct.new(:first_name, :last_name)
employee = Employee.new("John", "Doe")
employee.first_name # => "John"
employee.last_name # => "Doe"

As you can see, it behaves like a simple Ruby class. Above code is equivalent to:

class Employee
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end
end

employee = Employee.new("John", "Doe")
...

Want to receive useful tips, information about new Ruby gems and articles on a daily basis? Make sure you follow me and say hello!

What if we want to define the #full_name name on our Employeeclass? We can do it with Struct as well:

Employee = Struct.new(:first_name, :last_name) do
  def full_name
    "#{first_name} #{last_name}"
  end
end

employee = Employee.new("John", "Doe")
employee.full_name # => "John Doe"

When to use Struct

Struct is often used to make code cleaner, format more structured data from hash or as a replacement for real-world classes in tests.

  1. Temporary data structure - the most popular example is geocoding response where you want to form Address object with attributes instead of a hash with the geocoded data.
  2. Cleaner code
  3. Testing - as long as Struct respond to the same methods as the object used in tests, you can replace it if it does make sense. You can consider using it when testing dependency injection.

When not to use Struct

Avoid inheritance from Struct when you can. I intentionally assigned Struct to constant in the above example instead of doing this:

class Employee < Struct.new(:first_name, :last_name)
  def full_name
    "#{first_name} #{last_name}"
  end
end

When your class inherit from Struct you may not realize that:

  1. Arguments are not required - if one of the passed arguments it's an object then calling a method on it will cause error
  2. Attributes are always public - it is far from perfect encapsulation unless you really desire such behaviour
  3. Instances are equal if their attributes are equal - Employee.new == Employee.new
  4. Structs don't wanted to be subclassed - it creates an unused anonymous class and it is mentioned in the documentation

Play with it

Access the class attributes the way you want:

person = Struct.new(:first_name).new("John")
person.first_name # => "John"
person[:first_name] # => "John"
person["first_name"] # => "John"

Use the equality operator:

Person = Struct.new(:first_name)
Person.new("John") == Person.new("John") # => true

Iterate over values or pairs:

Person = Struct.new(:first_name, :last_name)
person = Person.new("John", "Doe")
# Values

person.each do |value|
  puts value
end
# >> "John"
# >> "Doe"

# Pairs

person.each_pair do |key, value|
  puts "#{key}: #{value}"
end
# >> "first_name: John"
# >> "last_name: Doe"

Dig:

Address = Struct.new(:city)
Person = Struct.new(:name, :address)
address = Address.new("New York")
person = Person.new("John Doe", address)

person.dig(:address, :city) # => "New York"

Alternatives

Hash

Hash is also considered as a alternative to Struct. It is faster to use but has worse performance than its opponent (I will test it a little bit later in this article).

OpenStruct

OpenStruct is slower but more flexible alternative. Using it you can assign attribute dynamically and it does not require predefined attributes. All you have to do is to pass a hash with attributes:

employee = OpenStruct.new(first_name: "John", last_name: "Doe")
employee.first_name # => "John"
employee.age = 30
employee.age # => 30

Standard boilerplate

Although it may get annoying when you have to type it multiple times:

class Employee
  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end

  def full_name
    "#{first_name} #{last_name}"
  end
end

Gems

Don't want to use Struct or its alternatives? There is a gem for that! Actually, there are many gems. Some of them are https://github.com/tcrayford/Values and Smart Init but there are many more gems available. However, it seems to be a good reason to write separated article about it. Make sure you are subscribed to my newsletter to don't miss it.

Alternatives comparision

**Name** **Non existing attributes** **Dynamically add attribute** **Performance (lower is better)**
Struct raises error no 1
OpenStruct returns `nil` yes 3
Hash returns `nil` yes 2

Benchmarks:

I used the following code to measure the performance of the above solutions:

Benchmark.bm 10 do |bench|
  bench.report "Hash: " do
    10_000_000.times do { name: "John Doe", city: "New York" } end
  end

  bench.report "Struct: " do
    klass = Struct.new(:name, :age)
    10_000_000.times do klass.new("John Doe", "New York") end
  end

  bench.report "Open Struct: " do
    10_000_000.times do OpenStruct.new(name: "John Doe", city: "New York") end
  end
end

Results:

**user****system****total****real**
**Hash**7.3800000.0600007.4400007.456928
**Struct**3.2800000.0100003.2900003.283013
**Open Struct**19.2700000.12000019.39000019.415336

Conclusion:

Open Struct is the slowest and most flexible solution in our comparision. Struct seems to be the best choice.

Want to become a better Rails developer?
Download for free the Introduction Rails patterns book and dive into the world of refactoring and easy-testable Ruby code today.
Join over 1,000 developers already subscribed to my newsletter and download the book. You can unsubscribe anytime:

Subscribe and get the book!