35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Ruby Struct vs Data: Welk Value Object Past Bij Jouw Code?

Ruby Struct vs Data: Welk Value Object Past Bij Jouw Code?

TTB Software
ruby
Een praktische vergelijking van Ruby's Struct en Data klassen voor value objects. Wanneer gebruik je welke, prestatieverschillen, en echte patronen uit productie Ruby 3.2+ 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_a of 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.

T

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 Touch

Share this article

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