Data
Yesterday we looked at how Struct can be an alternative to full-blown classes for simple objects.
A more recent addition to Ruby is the Data class, which provides an even more lightweight approach to creating immutable value objects.
Person = Data.define(:name, :birth_date)
# We can instantiate a person using ordered arguments:
person = Person.new("Alice", Date.parse("1990-01-01"))
# => #<Person name="Alice", birth_date=#<Date: 1990-01-01>>
# Or we can use keyword arguments:
person = Person.new(name: "Alice", birth_date: Date.parse("1990-01-01"))
# => #<Person name="Alice", birth_date=#<Date: 1990-01-01>>
# Unlike Struct, Data requires all attributes to be provided
person = Person.new(name: "Alice")
# => ArgumentError: missing keyword: birth_date
# We can access attributes using dot notation:
person.name # => "Alice"
person.birth_date # => #
# But we can't modify them:
person.name = "Bob" # => NoMethodError: undefined method `name='
"use strict";
class Person {
constructor(name, birthDate) {
const missingFields = [];
if (name === undefined) missingFields.push("name");
if (birthDate === undefined) missingFields.push("birthDate");
if (missingFields.length > 0) {
throw new Error(
`Missing attributes: ${missingFields.join(", ")}`
);
}
this.name = name;
this.birthDate = birthDate;
Object.freeze(this); // Freeze the object to make it immutable
}
}
// The class we created can only be instantiated via ordered arguments:
const person = new Person("Alice", new Date("1990-01-01"));
// We can access attributes using dot notation:
person.name; // => "Alice"
person.birthDate; // => Mon Jan 01 1990 01:00:00
// But since we froze the object we can't modify them:
person.name = "Bob";
// TypeError: Cannot assign to read only property 'name' ...
When we need updated values on an existing Data object, we can use the with method to create a new instance with updated attributes:
person = Person.new(name: "Alice", birth_date: Date.parse("1990-01-01"))
# => #<Person name="Alice", birth_date=#<Date: 1990-01-01>>
updated_person = person.with(name: "Ananya")
# => #<Person name="Ananya", birth_date=#<Date: 1990-01-01>>
Data shares many of the features of Struct, like pattern matching and defining additional methods by passing a block to Data.define.
Which should you use?
Rule of thumb: If you're good with immutability and don't need any of the extra features of Struct, use Data instead.
Immutability Caveat
The immutability of Data objects is shallow, meaning that if any of the attributes are mutable objects (like arrays or hashes), they can still be modified after the Data object is created.
History
The Data class was introduced in Ruby 3.2, released on Christmas 2022.