Ruby Struct vs Data: Welk Value Object Past Bij Jouw Code?
Ruby 3.2 introduceerde Data, een nieuwe core class voor immutable value objects. Als je al jaren Struct gebruikt en je afvraagt of je moet overstappen: gebruik Data wanneer je immutability op class-niveau wilt afdwingen, blijf bij Struct wanneer je mutability of positionele constructie nodig hebt.
Maar de details zijn belangrijk, dus laten we uitzoeken wanneer welke optie zinvol is.
Wat Data Je Geeft Dat Struct Niet Doet
Data.define maakt klassen waarvan de instanties bevroren zijn bij creatie. Geen writer methods. Geen []=. Geen onbedoelde mutatie:
Point = Data.define(:x, :y)
p = Point.new(x: 3, y: 7)
p.x # => 3
p.x = 10 # => NoMethodError
p.frozen? # => true
Bij Struct zou je handmatig moeten freezeën, en zelfs dan kun je nog de setter aanroepen (het gooit een FrozenError, geen NoMethodError). Data verwijdert de setters volledig uit de method table.
De constructie-API verschilt ook. Data.define instanties vereisen keyword arguments:
Point.new(3, 7) # => ArgumentError (missing keywords: x, y)
Point.new(x: 3, y: 7) # werkt
Struct accepteert zowel positionele als keyword arguments (sinds Ruby 3.1 met optionele keyword_init: true):
LegacyPoint = Struct.new(:x, :y)
LegacyPoint.new(3, 7) # werkt
LegacyPoint.new(x: 3, y: 7) # werkt ook in Ruby 3.2+
Wanneer Struct Nog Steeds Wint
Struct gaat nergens heen en is in meerdere scenario’s nog steeds de betere keuze.
Muteerbare records. Als je objecten bouwt die veranderen na creatie — state opbouwen tijdens een batchproces, coördinaten bijwerken terwijl iets door een pipeline beweegt — geeft Struct je writer methods uit de doos.
Positionele constructie. Wanneer je CSV-rijen of database-tuples verwerkt waar de kolomvolgorde bekend en stabiel is, is Struct.new(:name, :email, :role) met positionele args schoner dan elke keer keywords uitspellen.
Legacy compatibiliteit. Code die afhankelijk is van to_a die waarden retourneert in definitievolgorde, of members gebruikt om velden te itereren, werkt exact zoals voorheen met Struct.
Row = Struct.new(:name, :email)
row = Row.new("Alice", "alice@example.com")
row.to_a # => ["Alice", "alice@example.com"]
row[0] # => "Alice"
Entry = Data.define(:name, :email)
entry = Entry.new(name: "Alice", email: "alice@example.com")
entry.to_a # => NoMethodError
Prestaties: Marginaal Maar Meetbaar
Benchmark op Ruby 3.3.0, 1 miljoen instanties:
require "benchmark/ips"
S = Struct.new(:a, :b, :c)
D = Data.define(:a, :b, :c)
Benchmark.ips do |x|
x.report("Struct.new") { S.new(1, 2, 3) }
x.report("Data.new") { D.new(a: 1, b: 2, c: 3) }
x.compare!
end
Resultaten op een M2 MacBook Pro (Ruby 3.3.0):
Struct.new: 8.412.309 i/s
Data.new: 5.874.221 i/s - 1,43x langzamer
Keyword argument parsing voegt overhead toe. Voor de meeste applicaties is dit onzichtbaar. Maar in strakke inner loops of data-transformatiepipelines is Struct’s positionele constructie meetbaar sneller.
Het geheugengebruik is nagenoeg identiek. Beide alloceren dezelfde onderliggende objectstructuur.
Pattern Matching: Beide Werken Prima
Zowel Struct als Data werken met Ruby’s pattern matching, waar value objects echt tot hun recht komen:
case Point.new(x: 0, y: 5)
in Point[x: 0, y:]
puts "Op de Y-as op #{y}"
in Point[x:, y: 0]
puts "Op de X-as op #{x}"
end
Als je pattern matching uitgebreid gebruikt, integreert beide klassen naadloos.
Echte Patronen Uit Productie
Configuratieobjecten → Data. Config mag nooit veranderen na initialisatie:
AppConfig = Data.define(:database_url, :redis_url, :worker_count)
config = AppConfig.new(
database_url: ENV.fetch("DATABASE_URL"),
redis_url: ENV.fetch("REDIS_URL"),
worker_count: ENV.fetch("WORKER_COUNT", "5").to_i
)
Event payloads → Data. Events zijn feiten over iets dat gebeurd is. Ze horen niet te muteren:
OrderPlaced = Data.define(:order_id, :customer_id, :total_cents, :placed_at)
Tussentijdse verwerkingsrecords → Struct. Wanneer je data verrijkt over pipeline-stappen:
ImportRow = Struct.new(:raw_name, :normalized_name, :email, :valid, keyword_init: true)
row = ImportRow.new(raw_name: " Bob Smith ")
row.normalized_name = row.raw_name.strip
row.valid = row.email&.include?("@")
De Keuzechecklist
Kies Data wanneer:
- Het object een waarde of feit representeert (coördinaten, geld, events, configuratie)
- Je immutability-garanties op class-niveau wilt
- Je op Ruby 3.2+ zit
- Keyword constructie de leesbaarheid verbetert
Kies Struct wanneer:
- Het object state opbouwt of verandert tijdens zijn levensduur
- Positionele constructie past bij de use case (CSV parsing, tuple unpacking)
- Je
to_aof integer-index toegang nodig hebt - Maximale instantiatie-doorvoer belangrijk is
- Je Ruby < 3.2 moet ondersteunen
FAQ
Kan ik custom methods toevoegen aan Data klassen?
Ja. Geef een block mee aan Data.define:
Money = Data.define(:cents, :currency) do
def to_s
"#{cents / 100.0} #{currency}"
end
def +(other)
raise "Currency mismatch" unless currency == other.currency
Money.new(cents: cents + other.cents, currency: currency)
end
end
Omdat Data instanties bevroren zijn, moeten je custom methods nieuwe instanties retourneren in plaats van te muteren.
Werkt Data met ActiveRecord?
Data objecten zijn geen ActiveRecord modellen. Maar ze zijn handig als query result wrappers of value objects binnen AR modellen. De composed_of macro in Rails werkt met elke klasse die keyword arguments accepteert, dus Data klassen passen daar prima.
Hoe zit het met OpenStruct?
OpenStruct is een ander beest — het maakt dynamisch methods aan en is significant langzamer (circa 10x) dan zowel Struct als Data. Kies voor nieuwe code tussen Struct en Data. Beschouw OpenStruct als legacy.
Kan ik converteren tussen Struct en Data instanties?
Er is geen ingebouwde conversie, maar beide ondersteunen to_h:
struct_point = LegacyPoint.new(3, 7)
data_point = Point.new(**struct_point.to_h)
Dit werkt zolang de veldnamen overeenkomen.
About the Author
Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.
Get in TouchRelated Articles
Need Expert Rails Development?
Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise
Schedule a Free Consultation