Ruby adds a new core class called Data to represent simple immutable value objects


Ruby 3.2 adds a new core class called Data to represent simple immutable value objects. The Data class helps define simple classes for value-alike objects that can be extended with custom methods.

While the Data class is not meant to be used directly, it can be used as a base class for creating custom value objects. The Data class is similar to Struct, but the key difference being that it is immutable.

Discussions started in Ruby forums to build a data model on the principles of the Value Object, introduced by Martin Fowler. It has the following properties:

  • Immutable
  • Compared by type & value
  • Extensible

Let’s look at an example of defining a Data class that represents a person.

  irb(main):001:0> Person = Data.define(:name, :dob)
  => Person

Data.define takes a list of symbols and returns a new class. Each symbol is an member of the newly defined Person class.

Now let’s use the Person class to create a new person object.

  irb(main):002:0> john = Person.new("John", Date.new(1990, 1, 1))
  => #<Person:0x00007f9b0c0b0a00 @name="John", @dob=#<Date: 1990-01-01 ((2447892j,0s,0n),+0s,2299161j)>>
  irb(main):003:0> john.name
  => "John"
  irb(main):004:0> john.dob.to_s
  => "1990-01-01"

However remember that the Person class is immutable. Let’s try to change the name of the person.

  irb(main):005:0> john.name = "John Doe"
  (irb):5:in `<main>': undefined method `name=' for #<data Person name="John", dob=#<Date: 1990-01-01 ((2447893j,0s,0n),+0s,2299161j)>> (NoMethodError)
  Did you mean?  name

This is the main difference between Data and Struct. Struct is mutable, while Data is immutable.

Let’s define the same Person class using Struct to see the difference.

  irb(main):006:0> PersonStruct = Struct.new(:name, :dob)
  => PersonStruct
  irb(main):007:0> john_struct = PersonStruct.new("John", Date.new(1990, 1, 1))
  => #<struct PersonStruct name="John", dob=#<Date: 1990-01-01 ((2447892j,0s,0n),+0s,2299161j)>>
  irb(main):008:0> john_struct.name = "John Doe"
  => "John Doe"
  irb(main):009:0> john_struct.name
  => "John Doe"

However there is one caveat. If some of the data members are of a mutable class, Data does no additional immutability enforcement.

  irb(main):010:0> Person = Data.define(:name, :dob, :address)
  => Person
  irb(main):011:0> john = Person.new("John", Date.new(1990, 1, 1), {street: "123 Main St"})
  => #<Person:0x00007f9b0c0b0a00 @name="John", @dob=#<Date: 1990-01-01 ((2447892j,0s,0n),+0s,2299161j)>, @address={:street=>"123 Main St"}>
  irb(main):012:0> john.address[:street] = "456 Main St"
  => "456 Main St"
  irb(main):013:0> john.address
  => {:street=>"456 Main St"}

Ruby maintainers decided that Struct could not be made to suit the requirements of a value object. The core Struct class is a “somewhat alike” value-object; it is compared by value and is,

  • mutable
  • collection-alike (defines to_a and is Enumerable)
  • dictionary-alike (has [] and .values methods)

Let’s look at this in action.

  irb(main):010:0> Array(john_struct)
  => ["John Doe", #<Date: 1990-01-01 ((2447893j,0s,0n),+0s,2299161j)>]
  irb(main):011:0> Array(john)
=> [#<data Person name="John", dob=#<Date: 1990-01-01 ((2447893j,0s,0n),+0s,2299161j)>>]

The expectation for the struct object is that it forms an array of structs, however the struct object is deconstructed and its values are used to form the Array. However the data object returns an array of Data objects. This is because the Data class is not Enumerable.

  irb(main):012:0> john_struct[:name]
  => "John Doe"
  irb(main):013:0> john[:name]
  => nil

This also means that the Data class will not inherit the Enumerable module, which provides methods like #include?, #count, #map, #select and #uniq, amongst others. It is truly a simple value object. It is only meant to be a storage for immutable atomic values.

Further, the struct object is also dictionary-alike, which means that it can be accessed using the .values method. However the Data object is not dictionary-alike.

  irb(main):014:0> john_struct.values
  => ["John Doe", #<Date: 1990-01-01 ((2447893j,0s,0n),+0s,2299161j)>]
  irb(main):015:0> john.values
  => nil
  irb(main):016:1* def display(arg1, arg2 = "Empty!")
  irb(main):017:1*   puts arg1, arg2
  irb(main):018:0> end
  => :display
  irb(main):019:0> display *john_struct
  John Doe
  1990-01-01
  => nil
  irb(main):020:0> display *john
  #<data Person name="John", dob=#<Date: 1990-01-01 ((2447893j,0s,0n),+0s,2299161j)>>
  Empty!                              
  => nil

However, it is worth noting that the Data class does have a #to_h method.

  irb(main):021:0> john_struct.to_h
  => {:name=>"John Doe", :dob=>#<Date: 1990-01-01 ((2447893j,0s,0n),+0s,2299161j)>}
  irb(main):022:0> john.to_h
  => {:name=>"John", :dob=>#<Date: 1990-01-01 ((2447893j,0s,0n),+0s,2299161j)>}

Finally, it is possible to extend the Person class with additional methods, only during class definition.

  irb(main):023:1* Person = Data.define(:name, :dob) do
  irb(main):024:2*   def <=>(other)
  irb(main):025:2*     return unless other.is_a?(self.class)
  irb(main):026:2*     (Date.today - dob) <=> (Date.today - other.dob)
  irb(main):027:1*   end
  irb(main):028:1* 
  irb(main):029:1*   include Comparable
  irb(main):030:0> end
  => Person

Now let’s compare the two Person objects.

  irb(main):031:0> john = Person.new("John", Date.new(1990, 1, 1))
  => #<Person:0x00007f9b0c0b0a00 @name="John", @dob=#<Date: 1990-01-01 ((2447892j,0s,0n),+0s,2299161j)>>
  irb(main):032:0> jane = Person.new("Jane", Date.new(1988, 1, 1))
  => #<Person:0x00007f9b0c0b0a00 @name="Jane", @dob=#<Date: 1990-01-01 ((2447892j,0s,0n),+0s,2299161j)>>
  irb(main):033:0> john > jane
  => false

The new Data class is a simple value object. While it is not meant to be a replacement for Struct, it can be used to store immutable atomic values making it easier for Ruby developers to create simple structures to store immutable data.

Will you be using the Data class in your next project? Let us know!

Join Our Newsletter