Be nice to others and your future-self: use Data Objects.
In Ruby, hashes and arrays are the go-to data structures. For example, Rails turns http params into a hash and a JSON payload is a hash with nested arrays and hashes. The flexibility those primitives offer is undeniable and is key to developer happiness.
There are times where those data structures become too complex and we have to dig deeper into the codebase to figure what a hash is supposed to contain. This is where Data Objects can help us a great deal.
Data Objects contain data; they don’t implement any behaviour. A Struct is the simplest Data Object you can think of. It has a defined set of attributes that are publicly available as instance methods.
The main benefit of Data Objects is that they explicitly define the attributes available.
This self-documentation is a great gift for other developers (and your future-self). A quick look at the Data Object definition tells us what attributes are available. No need to go through those four classes where a hash that’s returned by a third-party API is transformed, filtered, reduced, deleted, symbolized keys and recursively flattened… wait, what?!
It also leads to a nicer syntax and better error messages:
Virtus, the Data Object’s best friend
I fell in love with Virtus a couple years ago. It has a great syntax to define object attributes.
User has setters and getters for the attributes defined and it can be initialized with a hash of attributes (using symbols or strings as keys):
The syntax to define attributes works as excellent documentation. Ruby is not a typed language, but Virtus provides some sort of a feel of optional typing.
As you can see in the example above, Virtus attempts to coerce values to the type passed in. This is great for consuming APIs or http params:
Strings will be converted to
Time if you’ve defined your attribute to be so. Even better, nested arrays and hashes can be coerced to another Virtus model.
Last but not least, the
#attributes method turns your Virtus Data Objects back into primitives making Virtus a great serializer.
Wrapping API responses with Virtus
Let’s take the Mandrill API as an example here. The
/messages/info.json returns information about an email you sent including sender, subject, opens, clicks as well as all open and click events.
The ruby wrapper turns that JSON into (oh, surprise!) a large hash. You could query the hash via
reponse.fetch('metadata').fetch('user_id') and look up the online documentation to determine what’s available. Let’s create a Data Object to wrap the API responses here. Using Virtus and some code-editing-fu it takes a couple of seconds to turn the documentation into a Data Object class.
Below is an excerpt of the
MandrillMessage definition. The types and comments are just taken out of the html documentation. The list of “open details” will turn into a nested array of
Let’s give this a try and turn the hash returned by the API client into a
Serializing data with Virtus
It is really easy to serialize a Virtus object into a database or a cache store. Say you want to persist a Report in an ActiveRecord model. We define the
report attribute as a
:json column and wrap it with a
Report Data Object.
Data Objects > Hashes
Hashes and primitives are great but they can be obscure or hard to manage at times. I find Data Objects way easier to reason about. Data Objects self-document the code and make it more reliable. So the next time you deal with a complex data structure, do yourself a favor and turn it into a Data Object with Virtus. You’ll thank yourself later.