The Ruby Craftsman
The Power of Ruby Structs
I invite you to explore beyond ActiveRecord models and into the world of PORO (Plain Old Ruby Objects). These are the building block of any Object Oriented Language and especially Ruby. I’m going to focus on Value Objects, which holders of some collection of data or attributes. The collection could model a person, an account, an address, or more abstractly an error, and many more. You should keep in mind as a general principle that these objects should have a well-defined purpose or identity and contain no more information than what is needed.
A simple and common way to pass around a collection of attributes is with a Hash. It’s flexible for taking any number of key-value pairs created with ease and manipulatable. It can be passed back and forth between objects. So why would want to use anything else? The ability to decouple different parts of the code and get a quicker understanding of objects being passed around. All the of these also can lead to better truthful tests. For example, if there is one part of the code responsible for populating a Hash, that represents a person and another part that takes it as an input. If you were testing those two parts in isolation, you’d have one test that asserts that object 1 emmets the correct Hash. You’d commonly copy and paste that person Hash into the next test of object 2. The test would pass all is good. Then a feature request comes along that requires changing the key name in the person Hash and because it was a copy and paste job object 2 test still passes, but then you run it in production and things are broken. If you would have instead used a defined object like a Struct changing its interface would have given you feedback in all the place it was used. This sets up an interface object between any interacting objects.
The simplest way to get started, and which also happens to be the most flexible, is with an initializer and attr_readers.
Handwritten Version
With this version, you are in control of everything, and you can see exactly what is happening, nothing is hidden behind some particular DSL. If you want to make a keyword optional def initialize(name:, age: nil) you can add a default argument of nil or any other value that makes sense as a default.
Struct
The next Ruby construct I want to talk about is Struct
. It removes some of the boilerplate necessary in the handwritten version.
Wow, that’s a lot less code! In the past, I’ve passed on using Struct because it only supported positional arguments, which I find less clear and harder to refactor in the future.
With Keyword Arguments
But a new feature is available keyword_init: true
.
Sadly we lose the feature of required keywords, they all work as being optional.
Then again we gain a lot of other features, all of which are possible in the Handwritten version, but with a whole lot less code. I’m going to go through some more included features and show you how to add these feature in the Handwritten version.
Inspectable
Compare that to our handwritten version #<Person:0x00007fe0650913f0>
. Not very readable or useful in most cases. Of course, there is a way to overwrite this string representation by defining your own #to_s
.
Results in:
Enumerable
Each
Yields the value of each struct member in order.
The Handwritten version would require you to write a conversion method #to_h
.
From there you could call #to_h
and that would enable you to invoke all enumerable methods or to include the Enumerable module and also define an #each
method if want direct access to call these methods.
The #each
method delegates to the hash version of the object. You could also refactor that to use the Forwardable
module.
Each Pair
Yields the name and value of each struct member in order.
Equality
Here’s an example from the Ruby documentation showing how the comparison works with Structs.
This is how you would add that feature to your own handwritten object by defining the spaceship operator #<=>
and including the Comparable
module.
Adding Methods
This is the recommended way to customize a struct. Subclassing an anonymous struct creates an extra anonymous class that will never be used.
Members
Here is a bonus trick. If ever have a larger set of data and you want to selectively pull from it without individually referencing each key #members
could be useful.
If I try to put more than what this struct expect I get an argument error. (Remember that keywords and hashes are interchangeable)
So here is the brute force method of only inputting the attributes that are relevant.
In most simple cases you’ll want to use this approach even though it more verbose than the next example.
And now the dynamic method.
You can do the same thing with the Handwritten version by grabbing the method proc and getting its parameters.
It works, but who wants to see that kind of code in their project?
If you have any thoughts on Ruby Structs or the handwriting versio feel free to leave a comment.