Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
How to create value objects in Ruby – the idiomatic way (ghinda.com)
42 points by unripe_syntax 10 months ago | hide | past | favorite | 18 comments


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')


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.

[1]: https://github.com/maxim/portrayal


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.


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.


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.


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.)


Nobody in Ruby is getting tripped up over

    Point = Data.new(:x, :y) do
      def len
        Math.sqrt(x**2 + y**2)
      end
    end
For one, Struct has existed since basically the beginning of time and works exactly the same way (sans the immutability and some nice auto-implemented features).


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.


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.


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


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


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.


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.


The article is demonstrating the API, not suggesting you define Data objects dynamically. Notice the objects are assigned to constants.


"Here's when and why to use a tool" is a really good thing to explain along with how to use it.


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?)


Great article, wasn't there a bit fuzz about the Data class being slower than OpenStruct in terms of performance?


The answer is, like most things, not simple; it depends on what you are doing and the version of Ruby you are using.

I just found this[1] great article by Miko Dagatan with loads of benchmarks for Ruby 3.4.2 that goes into a bit of depth.

1. https://reinteractive.com/articles/ruby-performance-structs-...




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: