Skip to Content
Course content

54: Struct and OpenStruct for Lightweight Objects

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

Why bother with Struct if I can just write a regular class?

I've seen a lot of developers spend way too much time writing "data classes"—classes that don't really have any complex logic and just exist to hold a few pieces of information. You know the drill: you define the class, write out the attr_accessor for every field, and then write an initialize method that just assigns those values to instance variables. It's tedious.

A Struct is essentially a shortcut for that entire pattern. It generates the class, the accessors, and the initializer for you in one line. Take a look at this example for a simple product in an inventory system:

# The tedious way
class Product
  attr_accessor :name, :sku, :price
  def initialize(name, sku, price)
    @name = name
    @sku = sku
    @price = price
  end
end

# The Struct way
Product = Struct.new(:name, :sku, :price)

laptop = Product.new("MacBook Pro", "MBP-14-2023", 1999.99)
puts laptop.name # => "MacBook Pro"

Beyond saving keystrokes, Struct gives you some great defaults. For instance, it overrides the == operator. If you compare two Struct objects with the same values, they'll be equal. If you did that with a standard class, Ruby would just check if they are the exact same object in memory, which usually isn't what you want for data containers.

When should I use OpenStruct instead of a Hash?

I'll be honest: you'll use OpenStruct much less often than Struct, but it has its place. Think of it as a Hash that lets you use dot notation. It's incredibly useful when you're dealing with data that is highly dynamic or when you're mocking objects for tests.

Imagine you're building a configuration loader where the settings might change depending on the environment. Using a Hash is fine, but config[:api_key] is just a bit clunkier than config.api_key.

require 'ostruct'

# Creating a dynamic config object
config = OpenStruct.new(api_key: "secret_123", timeout: 30)

# I can add new attributes on the fly without defining them first
config.retries = 3

puts config.api_key # => "secret_123"
puts config.retries # => 3

If you know exactly what fields your object needs, use a Struct. If you're essentially creating a "bag of properties" that might change during runtime, OpenStruct is your friend.

What's the catch? When are these a bad idea?

Nothing in Ruby is free. The biggest trade-off with OpenStruct is performance. Because it uses method_missing under the hood to handle those dynamic calls, it is significantly slower than a Hash or a Struct. In a tight loop with thousands of iterations, OpenStruct will absolutely kill your performance. I generally avoid it in production code unless the convenience outweighs the cost.

As for Struct, the main "gotcha" is that it's very permissive. All attributes are public and writable by default. If you need strict encapsulation—meaning you want to validate data before it's set or keep certain fields private—you have to go back to a full-blown class.

I usually follow this rule of thumb:

  • Need a quick, immutable-ish data container? Struct.
  • Need a flexible, "schemaless" object for a config or a mock? OpenStruct.
  • Need logic, validation, or private state? Class.



📋 Practical Task

Exercise: Building a Cinema Ticket System

You are building a small system to track movie tickets. Your goal is to use Struct for the fixed data and OpenStruct for the variable metadata.

  1. Create a Struct named Ticket with the following attributes: movie_title, seat_number, and price.
  2. Instantiate a Ticket object for "Inception", seat "A12", priced at 15.00.
  3. Create an OpenStruct object called ticket_meta. Use it to store any additional, non-standard information about the ticket (e.g., concessions_preordered: true or customer_loyalty_id: "L-99").
  4. Write a small piece of code that prints a summary using both objects, for example: "Ticket for Inception (Seat A12) costs $15.00. Loyalty ID: L-99".
Rating
0 0

There are no comments for now.

to be the first to leave a comment.