How to create value objects in Ruby – the idiomatic way
17 comments
·March 20, 2025hakunin
For slightly more advanced value objects, check out the gem[1] I wrote. It has a bit more depth in how it "enhances" every attribute to behave as you'd expect, to improve the object's clone-ability, freezability, inheritability, declarative style of attribute definition, and a few more perks. All while remaining super small/simple in the way it's implemented.
viralpraxis
One thing people usually do is
D = Data.define do E = new end
which is wrong since ’E‘ escapes (it becomes ´Object::E´)
also, I agree that its kinda weird to use Ractor API but for us (service object gem) it was the best way to check if an argument is immutable (via Ractor.shareable?)
davidw
The article does not do a great job of explaining why creating new classes dynamically is preferable to just defining the class your system needs.
dragonwriter
Its Ruby, there is no way of creating classes that is anything other than dynamic creation at runtime. The class keyword creates classes no less dynamically than any other method, and assigns the created class to the constant name provided after the keyword. Using a class factory method like Data.define and assigning the result to a constant does the same thing as using the class keyword.
You are literally imagining a distinction that does not exist in Ruby.
davidw
No need to go all 'comic book guy'. What I'm talking about, of course, is the way that people expect classes to be defined in Ruby, which is going to be less confusing and easier to read because it's what everyone is used to.
dragonwriter
Class factory methods like Struct.new have been a idiomatic and familiar-to-everyone-using-Ruby and commonly encountered way of creating classes in Ruby for at least as long as I have been using Ruby (since about the turn of the millenium.)
pclowes
I think it is just a more ergonomic, lighter weight, and intention revealing alternative.
If I see a data class instantiation. I immediately have an idea of its scope and what could/couldnt go wrong with it.
Nuzzerino
It’s not. You normally can’t marshal dynamic classses or their instances across process boundaries so your multithreading options are limited. But maybe the data class has a way to do that. I see no advantage to not use concrete classes if those are good enough to get the job done.
stouset
All classes in Ruby are dynamic.
There is no effectively no difference between these:
class Foo < Bar
attr_accessor :baz
end
Foo = Class.new(Bar) do
define_method(:baz) do
@baz
end
end
dragonwriter
I'm not sure what you are talking about; all classes are equally dynamic in Ruby and a class defined at the same point in the code with a factory method like Data.define and assigned to a constant can marshalled across process boundaries exactly as well as one defined with the class keyword, not as a special feature of the Data class but because using a class factory method and assogning the result to a constant is simply functionally the same thing as defining a class using the class keyword in Ruby.
The idea of distinct “concrete” and “dynamic” classes seems to be a product of a wrong mental model of how Ruby is executed.
abid786
Not sure I follow. The constants like Price or Currency would be available across threads.
Also, if you’re writing a custom data class, you’d need to write code to support immutability which this article’s approach handles
chowells
In Ruby, the class keyword is evaluated at runtime and the resulting object is crammed into a global mutable namespace tree. That is, it's dynamic too.
Just not in quite the same way.
bullfightonmars
The article is demonstrating the API, not suggesting you define Data objects dynamically. Notice the objects are assigned to constants.
davidw
"Here's when and why to use a tool" is a really good thing to explain along with how to use it.
Alifatisk
Great article, wasn't there a bit fuzz about the Data class being slower than OpenStruct in terms of performance?
null
I don't usually think of Data when grouping values together in Ruby. Seems like I should. Lucian puts forth a good explainer of when and why they are helpful.
To summarize, Data became available a few years ago in Ruby 3.2. You can create value objects by subclassing the Data class with Data.define. Data objects are immutable, comparable and easily greppable. They are constrained in ways Struct, Hash and Class are not. The shorthand removes boilerplate and the constraints create the utility.
Measure = Data.define(:amount, :unit) weight = Measure.new(amount: 50, unit: 'kg')