Skip to Content
Course content

64: Struct and OpenStruct

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this all the time in junior-to-mid-level codebases: someone discovers OpenStruct and starts using it as a universal replacement for Hashes because they prefer user.name over user[:name]. There is a common belief that OpenStruct is essentially a "magic Hash" that gives you the best of both worlds—the flexibility of a dictionary and the clean syntax of an object.

The myth that OpenStruct is a lightweight Hash replacement

It feels lightweight, but under the hood, it's a performance nightmare. When you assign a value to an OpenStruct, Ruby doesn't just store a key-value pair; it dynamically defines a method on that specific object's singleton class. If you're creating thousands of these in a loop, you're hammering the Ruby VM with method definitions, which kills your memory and slows your execution to a crawl.

require 'ostruct'

# This looks clean...
person = OpenStruct.new(name: "Alice", role: "Engineer")
puts person.name # "Alice"

# ...but every time you do this, Ruby is doing a lot of heavy lifting
# behind the scenes to make that .name method exist.

If you just need a bag of data that doesn't change its shape, OpenStruct is almost always the wrong tool. It's meant for very specific cases where you truly don't know what the attributes will be until runtime. For everything else, we have Struct.

Struct: Defining a blueprint for your data

Think of a Struct as a shortcut for creating a class that only exists to hold data. Instead of writing a full class with an initialize method and five different attr_accessor declarations, you can do it in one line. Unlike OpenStruct, a Struct defines the methods once on the class level, making it nearly as fast as a regular Ruby class.

Let's say we're building a system to track packages in a warehouse. We know every package has a tracking number, a weight, and a destination.

# This creates a new Class named Package
Package = Struct.new(:tracking_number, :weight, :destination)

# Now we can instantiate it
pkg = Package.new("ABC-123", 4.5, "New York")

puts pkg.tracking_number # "ABC-123"
pkg.weight = 5.0         # Structs are mutable by default

I'll give you a pro tip here: in older Ruby versions, Struct only accepted positional arguments. If you had ten fields, you had to remember the exact order. Now, we can use keyword_init: true to make the code much more readable and less prone to "off-by-one" errors when passing arguments.

# Much better for larger data objects
Package = Struct.new(:tracking_number, :weight, :destination, keyword_init: true)

pkg = Package.new(
  tracking_number: "XYZ-789", 
  weight: 1.2, 
  destination: "London"
)

Choosing between the two based on the "Shape" of your data

When I'm reviewing a PR, I use a simple rule of thumb to decide which one to use. Ask yourself: "Do I know the names of the attributes before the program even runs?"

  • Yes: Use Struct. It's faster, uses less memory, and acts as a form of documentation for other developers.
  • No (the data is truly dynamic): Use a Hash. If you absolutely must have dot-notation for some reason (like a configuration object), only then consider OpenStruct, but be mindful of the cost.

Most of the time, if you find yourself reaching for OpenStruct, you're actually just craving a Struct with keyword arguments.




📋 Practical Task

Refactoring the Game Character Registry

You have been handed a piece of legacy code for a game engine. The original developer used OpenStruct to handle character stats, and as the game grew to thousands of NPCs, the memory usage spiked and the frame rate dropped.

Your goal: Refactor the Character implementation from an OpenStruct to a Struct with keyword initialization to improve performance and maintainability.

require 'ostruct'

# LEGACY CODE - Refactor this!
def create_character(name, hp, mp)
  OpenStruct.new(name: name, hp: hp, mp: mp)
end

# Test cases
hero = create_character("Valerius", 100, 50)
puts "#{hero.name} has #{hero.hp} HP"

Requirements:

  1. Define a Character Struct with :name, :hp, and :mp.
  2. Enable keyword_init: true for the Struct.
  3. Update the create_character method to return a Character instance using keyword arguments.
  4. Ensure the final puts statement still works exactly as it did before.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.