Struct, Data and Value Objects

20.1 Struct

Point = Struct.new(:x, :y) do
  def distance_to(other) = Math.hypot(x - other.x, y - other.y)
  def to_s = "(#{x}, #{y})"
end
a = Point.new(0, 0)
b = Point.new(x: 3, y: 4)              # keyword init (Ruby 3.2+ accepts keywords by default)
a.distance_to(b)                       # 5.0
a.x = 10                               # Structs are mutable
a == Point.new(10, 0)                  # value equality
x, y = *a                              # to_a destructuring
a.to_h
Customer = Struct.new(:name, :email, keyword_init: true)

20.2 Data (Ruby 3.2+: Immutable Value Objects)

Money = Data.define(:amount, :currency) do
  def +(other)
    raise ArgumentError, "currency mismatch" unless currency == other.currency
    with(amount: amount + other.amount)     # returns a modified copy
  end
  def to_s = format("%.2f %s", amount, currency)
end

price = Money.new(amount: 10, currency: "USD")
total = price + Money.new(20, "USD")
total.amount                   # 30
# total.amount = 5             # NoMethodError: immutable
case total
in { amount: Integer | Float => amt, currency: "USD" } then puts "USD #{amt}"
end

20.3 OpenStruct and Alternatives

Prefer Struct, Data, plain hashes, or proper classes. OpenStruct is slow and dynamic.

require "ostruct"               # a bundled gem in Ruby 3.5/4.0+; slow, so avoid it in hot code
config = OpenStruct.new(host: "localhost", port: 8080)
config.port; config.debug = true