Intro 🗃

Recently there’s been some discussion about introducing some new core functionality into the Ruby language as a basis for Value Objects. Then the new Data class has been merged into Ruby and is now expected be included into the next releases. We’re expecting to get it with the Ruby 3.2 release this Christmas 🎄

What is it

The new class delivers a Struct-like functionality for defining classes which you can then use for immutable Value Objects. It is lightweight, easy to use, and gives you some immutability out of the box. It’s something that a lot of projects have built in some form or another, though as a boilerplate. Here you have it right out of the box with the language!

How to get it

You can just install ruby version 3.2.0-dev from rbenv or rvm, or just build ruby from its GitHub repo, and it should work! Or just wait for the Ruby 3.2 release this Christmas 🎄

Using it

It’s easy to define such Data classes. The most common use case, as with Struct, would be to define it inside of another class (like a Service Class), and then play around with the tiny Data class inside, for the ease of processing.

In order to be more concise here, I will only define the tiny Coffee class here, not wrapped into anything, but we can imagine a much more complex class covering (wrapping) the Coffee class (like a CoffeePlace or Barista that brews multiple instances of Coffee for the period of its own existence).

Coffee = Data.define(:country, :roast, :made_with, :recipe_url) do
  def announce
    puts "Your coffee is ready! It's made of #{country} beans, #{roast} roast, equipment is #{made_with}, recipe: #{recipe_url}"
  end
end

So what exactly is this? We’ve just defined a new class called Coffee which has some features out of the box. I will list those features after we have a look at them. For now, you can see that we’ve given the class some attributes – country, roast, made_with and recipe_url. These will be necessary to provide when creating an instance of Coffee.

I’ve also defined the custom #announce method which will be available for every instance of Coffee.

Let’s finally create some objects of that new Coffee class.

coffee = Coffee.new(
  country: 'Colombia',
  roast: :light,
  made_with: :aeropress,
  recipe_url: 'https://aeroprecipe.com/recipes/james-hoffmann-aeropress-recipe'
)
=> #<data Coffee country="Colombia", roast=:light, made_with=:aeropress, recipe_url="https://aeroprecipe.com/recipes/james-hoffmann-aeropress-rec...
Let’s take a look at the object now.

coffee.roast
=> :light

coffee.announce
Your coffee is ready! It's made of Colombia beans, light roast, equipment is aeropress, recipe: https://aeroprecipe.com/recipes/james-hoffmann-aeropress-recipe

Let’s try to change one of its attributes:

coffee.roast = :dark
(irb):26:in `<main>': undefined method `roast=' for #<data Coffee country="Colombia", roast=:light, made_with=:aeropress, recipe_url="https://aeroprecipe.com/recipes/james-hoffmann-aeropress-recipe"> (NoMethodError)

Makes sense! You can’t change a roast of a coffee sample, especially when it’s already brewed. We’re getting immutability out of the box, so you can make sure the objects stay the same way as created initially. However, there’s a caveat described in this great article by Swaathi Kakarla:

If some of the data members are of a mutable class, Data does no additional immutability enforcement.

This means there’s still some room for mutability, like changing values inside of a Hash key, so it makes sense to be careful with such attributes.

Summarizing the Features

  1. Easy class definition. You don’t have to set attr_reader and #initialize method. All you do is you list the attributes for the objects of the class. Very handy for defining small Data “structures” inside of other classes, no boilerplate.
  2. Immutability out of the box. Which means you are ensured that nothing gets accidentally changed, especially if there’s a long chain of an object being used in multiple places, or within some multi-thread logic.
  3. You can still optionally define methods inside of this class, just as with regular class definition. I’ve defined the #announce method which announces that the coffee has been brewed.
  4. You have some flexibility of argument passing when creating new instances. Namely you can provide the arguments with their keywords for some extra flexibility (a), or you can directly list the argument values as in mathematical functions or more traditional programming style (b)
    • (a) Coffee.new(country: ‘Colombia’, roast: :light, made_with: :v60, recipe_url: ‘recipes.com/1’)
    • (b) Coffee.new(‘Colombia’, :light, :v60, ‘recipes.com/1’)

Potential usages

I might actually see this feature being used not only inside of some service classes that process something and need some structural immutable objects instead of dealing with rather primitive types like Hash, but also as simple models that represent something that’s not persisted to the database.

In both cases they would all be rather simple classes with minimal amount of methods, and not directly related to a table from a database. Think Value Objects.

Ideas from my side Being exposed to Ruby on Rails and its philosophy on a daily basis, I would also expect the use case where we could simply inherit from a class somewhat close to this new Data class, and then just build our own “models” or Value Objects in a traditional way (not through the newly introduced #define method).

So instead of the code examples above, I would also like to have something like this:

class User < ValueObject # or inherit from Data
  # or include ValueObject instead of inheritance
  attr_init :name, :age
end

class Ticket < ValueObject
  attr_init :id, :user_id, :event_id, :start_at, :valid_until
end

However, this idea does bring some complications, like having to know a special keyword for defining the attributes.

I've posted my question here, and got some reasonable answers from matz and Victor Shepelev aka zverok (creator of this feature).

So let’s see how it goes as people start incorporating this new feature into their projects, and what needs and ideas might pop up in the Ruby and Ruby on Rails communities to maybe further extend this. So far I’m glad that such easy to use features are being provided out of the box, and I also enjoy the fact that they eliminate a lot of boilerplate.

You can also read this issue in my Newsletter