Ruby Data Class vs Struct



This content originally appeared on DEV Community and was authored by Kelly Popko

In practice, a main difference between instances of Data and Struct is mutability:

  • Instances of Data are immutable*
  • Instances of Struct are mutable

*Mutable values passed to instances of Data remain immutable by default.

Data also has fewer (and different) ‘built-in’ methods than Struct.

This post explores differences in syntax and behavior between Data and Struct.

Syntax comparison: Create New Instance

Data

House = Data.define(:rooms, :area, :floors)
ranch = House.new(rooms: 5, area: 1200, floors: 1)
# => #<data House rooms=5, area=1200, floors=1>

Struct

House = Struct.new(:rooms, :area, :floors, keyword_init: true)
ranch = House.new(rooms: 5, area: 1200, floors: 1)
# => #<struct House rooms=5, area=1200, floors=1>

Mutability: Attempt to Renovate the house…

Data

House = Data.define(:rooms, :area, :floors)
ranch = House.new(rooms: 5, area: 1200, floors: 1)
# => #<data House rooms=5, area=1200, floors=1>

ranch.floors = 2
# (irb):3:in `<main>': undefined method `floors=' for an instance of House (NoMethodError)

This tells us there is no ‘writer’ (sometimes called “setter” in other
languages) method to use.

If we try to define a writer method on House and update that value, FrozenError is raised.

House = Data.define(:rooms, :area, :floors) do
  def floors=(quantity)
    @floors = quantity
  end
end
# => House

cottage = House.new(rooms: 5, area: 700, floors: 1)
# => #<data House rooms=5, area=700, floors=1>

cottage.floors = 2
# (irb):21:in `floors=': can't modify frozen House: #<data House rooms=5, area=700, floors=1> (FrozenError)
cottage.frozen?
# => true

What we can do with a Data object is clone it and update any of the attribute values.

House = Data.define(:rooms, :area, :floors)
cottage = House.new(rooms: 5, area: 1200, floors: 1)
# => #<data House rooms=5, area=1200, floors=1>

two_story_cottage = cottage.with(floors: 2)
# => #<data House rooms=5, area=1200, floors=2>

Struct

With Struct, we can renovate.

House = Struct.new(:rooms, :area, :floors, keyword_init: true)
ranch = House.new(rooms: 5, area: 1200, floors: 1)
# => #<struct House rooms=5, area=1200, floors=1>

ranch.floors = 2
# => 2

ranch
# => #<struct House rooms=5, area=1200, floors=2>

ranch.frozen?
# => false

Built-in Methods

Data Class

>> Data.methods(false)
=> [:define]
>> Data.instance_methods(false)
=>
[:deconstruct_keys,
 :pretty_print,
 :pretty_print_cycle,
 :deconstruct,
 :hash,
 :==,
 :inspect,
 :members,
 :to_s,
 :with,
 :eql?,
 :to_h]

Struct

>> Struct.methods(false)
=> [:new]
>> Struct.instance_methods(false)
=>
[:deconstruct_keys,
 :==,
 :members,
 :to_a,
 :to_s,
 :[],
 :[]=,
 :values_at,
 :eql?,
 :to_h,
 :select,
 :filter,
 :pretty_print,
 :pretty_print_cycle,
 :deconstruct,
 :hash,
 :inspect,
 :each_pair,
 :values,
 :length,
 :dig,
 :size,
 :each]

Comparison

>> struct_methods - data_methods
=>
[:to_a,
 :[],
 :[]=,
 :values_at,
 :select,
 :filter,
 :each_pair,
 :values,
 :length,
 :dig,
 :size,
 :each]

>> data_methods - struct_methods
=> [:with]


This content originally appeared on DEV Community and was authored by Kelly Popko