Examples of Value Objects with Ruby's Data Class

Ask questions Research chat →

https://allaboutcoding.ghinda.com/example-of-value-objects-using-rubys-data-class · scraped

deploy

Attachments

Scraped Content

— 660 words · 2026-05-19 12:34:41 UTC ·

Excerpt

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1742979806403/973bead1-7a96-42e2-bf87-7cddc1aba4ef.png?w=1600&h=840&fit=crop&crop=entropy&auto=compress,format&format=webp) Last week, I wrote an article about how to create value objects in Ruby - the idiomatic way. This week, I will share some real examples of using the data object to show some real examples. ## Remove boilerplate constructor code If you are defining classes and expose the initializer parameters as getters and you plan to make them immutable, then I think you just found the most common case for using the Data class: Instead of this: ```plain text class Link attr_reader :url, :source def initialize(url:, source:) @url = url @source = source end end ``` I write this: You can of course also write the simple form, but I do recommend the the previous way with inheritance (will followup in another article about this): ## When calling an external API When calling an external API that returns JS
![](https://cdn.hashnode.com/res/hashnode/image/upload/v1742979806403/973bead1-7a96-42e2-bf87-7cddc1aba4ef.png?w=1600&h=840&fit=crop&crop=entropy&auto=compress,format&format=webp) Last week, I wrote an article about how to create value objects in Ruby - the idiomatic way. This week, I will share some real examples of using the data object to show some real examples. ## Remove boilerplate constructor code If you are defining classes and expose the initializer parameters as getters and you plan to make them immutable, then I think you just found the most common case for using the Data class: Instead of this: ```plain text class Link attr_reader :url, :source def initialize(url:, source:) @url = url @source = source end end ``` I write this: You can of course also write the simple form, but I do recommend the the previous way with inheritance (will followup in another article about this): ## When calling an external API When calling an external API that returns JSON, I like to implement a method that returns a Response object. Here I define a Response object with two computed properties: - parsed_body - success ```plain text class Response < Data.define(:body, :status, :headers) HTTP_SUCCESS_STATUS_CODES = (200..299) def success? = HTTP_SUCCESS_STATUS_CODES.include?(status) def parsed_body = JSON.parse(body, symbolize_names: true) def failed? = !success? end ``` Mind you that you cannot memoize using instance variables inside a Data class due to immutability. If you try something like this you will get FrozenError ```plain text class Response < Data.define(:body, :status, :headers) def parsed_body @parsed_body ||= JSON.parse(body, symbolize_names: true) end end r = Response.new(body: "{}", status: 200, headers: {}) r.parsed_body # can't modify frozen Response: #<data Response body="{}", status=200, headers={}> (FrozenError) ``` A more full example might look like this using httparty gem ```plain text require 'httparty' require 'json' class Response < Data.define(:body, :status, :headers) HTTP_SUCCESS_STATUS_CODES = (200..299) def success? = HTTP_SUCCESS_STATUS_CODES.include?(status) def parsed_body = JSON.parse(body, symbolize_names: true) def failed? = !success? end def get(url, query: {}) response = HTTParty.get(url, query) Response.new(body: response.body, status: response.code, headers: response.headers) end response = get( 'https://bsky.social/xrpc/' \ 'com.atproto.identity.resolveHandle?handle=lucianghinda.com') puts response.parsed_body[:did] # did:plc:1362asasdah213212 puts response.success? # true ``` From this example you can for example expand it to add a RateLimit object: ```plain text require 'httparty' require 'json' class RateLimit < Data.define(:limit, :remaining, :reset) end class Response < Data.define(:body, :status, :headers, :rate_limit) HTTP_SUCCESS_STATUS_CODES = (200..299) def success? = HTTP_SUCCESS_STATUS_CODES.include?(status) def parsed_body = JSON.parse(body, symbolize_names: true) def failed? = !success? end def get(url, query: {}) response = HTTParty.get(url, query) rate_limit = RateLimit.new( limit: response.headers['ratelimit-limit'].to_i, remaining: response.headers['ratelimit-remaining'].to_i, reset: response.headers['ratelimit-reset'].to_i ) Response.new( body: response.body, status: response.code, headers: response.headers, rate_limit: rate_limit ) end ``` You can even add a constructor to RateLimit for example: ```plain text class RateLimit < Data.define(:limit, :remaining, :reset) def self.from_headers(headers) limit = headers['ratelimit-limit'].to_i remaining = headers['ratelimit-remaining'].to_i reset = headers['ratelimit-reset'].to_i new(limit: limit, remaining: remaining, reset: reset) end end ``` Here is an example I found in TheOdinProject: ```plain text # Source: https://github.com/TheOdinProject/theodinproject/app/models/flag.rb class Flag < ApplicationRecord Reason = Data.define(:name, :description, :value) REASONS = [ { name: :broken, description: 'Link does not work', value: 10 }, { name: :insecure, description: 'Link is not secure or safe', value: 20 }, { name: :spam, description: 'Spam or misleading', value: 30 }, { name: :inappropriate, description: 'Inappropriate imagery or language', value: 40 }, { name: :other, description: 'Other', value: 50 } ].map { |reason| Reason.new(**reason) } end ``` Furthe on you will see they are using REASONS in a Rails enum: And then in the view as a list of choices: ```plain text <% Flag::REASONS.each do |reason| %> <div class="relative flex items-center"> <div class="absolute flex h-6 items-center"> <%= form.radio_button( :reason, reason.name, data: { test_id: "flag-reason-#{reason.name}"}, class: 'h-4 w-4 border-gray-300 dark:border-gray-500 dark:bg-gray-700/50' # ... %> </div> <div class="pl-7 text-sm leading-6"> <%= form.label( :reason, reason.description, value: reason.name, class: 'block text-sm font-medium text-gray-700 dark:text-gray-200 dark:text-gray-200' %> </div> </div> <% end %> ``` If you like this article: 👐 Interested in learning how to improve your developer testing skills? Join my live online workshop about goodenoughtesting.com - to learn test design techniques for writing effective tests 👉 Join my Short Ruby Newsletter for weekly Ruby updates from the community

Visibility

Visible to everyone

Reading Status

Related Bookmarks

My Note


Saved!

Annotations

Export as Markdown
+ Annotate selection

Add Annotation