Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
64: Struct and OpenStruct
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 considerOpenStruct, 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:
- Define a
CharacterStruct with:name,:hp, and:mp. - Enable
keyword_init: truefor the Struct. - Update the
create_charactermethod to return aCharacterinstance using keyword arguments. - Ensure the final
putsstatement still works exactly as it did before.
There are no comments for now.