In Ruby 3.2, a new class
Data was introduced as a way to define simple immutable value objects. A value object is a type of object that represents a value in a program, such as a point in 2D space or a date. The main advantage of value objects is that they are easy to understand, simple to use, and can improve the readability and maintainability of code. The proposal to add
Data class was accepted by Matz on the Ruby forum here.
How does it work?
Using the newly defined class
Data we can create a simple immutable object. These objects are designed to be small, self-contained, and represent a single concept in your application.
The class definition includes the name of the class, as well as a list of instance variables that the object will contain. Here's an example:
class Point < Data.define(:x, :y) end # Can be initialized using Positional or Keyword arguments point = Point.new(3, 4) OR Point.new(x: 3, y: 4) OR Point[3, 4] OR Point[x: 3, y: 4] => #<data Point x=3, y=4> # But using both Positional and Keyword arguments together will not work Point.new(2, y: 3) => `new`: wrong number of arguments (given 2, expected 0) (ArgumentError)
Once an instance of the object is created, its instance variables cannot be changed. This makes it easy to understand about the object's state and prevents bugs that can arise from unexpected changes to the object.
However, if we need to change any one instance variable by keeping the other variable same we can do that using
point2 = point.with(x: 10) => #<data Point x=10, y=4>
Data objects are immutable,
with method creates a copy of the object to update the arguments.
Note: If the
with method is called with no arguments, the receiver is returned as-is and no new copy is created.
Data.define also accepts an optional block that can be used to define custom methods for the immutable object. These methods can provide additional functionality to the object without compromising its immutability.
class Point < Data.define(:x, :y) def distance_from_origin Math.sqrt(x**2 + y**2) end end point = Point.new(3, 4) => #<data Point x=3, y=4> point.distance_from_origin => 5.0
Why not Struct?
Struct can also be used to define objects, there are some reasons why
Data might be a better choice in certain situations.
Data provides some safety checks that
Struct does not. For example,
Data prevents an object from being created with a missing argument, while
Struct allows this. This can help to prevent bugs and improve code safety.
Here's an example:
Measure = Struct.new(:amount, :unit) measure = Measure.new(30) # This works, but will fail with Data => #<struct Measure amount=30, unit=nil>
Point.new(x: 3) # Missing argument will raise error for Data => `initialize': missing keyword: :y (ArgumentError)
In this example, we've defined a
Measure struct that contains two instance variables:
unit. However, when we create a new instance of the object, we only provide one argument. This is allowed by
Struct, but it can lead to bugs if the code assumes that all the instance variables are present.
Data is a more explicit way of defining simple immutable objects. The
Data class makes it clear that the object is intended to be immutable, while with
Struct it's less clear.
Data is a powerful tool for defining simple immutable objects in Ruby 3.2. While it may not be suitable for all situations, its safety, performance, and clarity benefits make it a strong choice in many cases. By understanding how
Data works and its tradeoffs compared to other techniques, such as
Struct, you can make an informed decision about when to use it in your own code.