Creating Value Objects in Ruby

One of the best things about trying out a new programming language is that it always teaches you something new about the languages you already know. That happened to me with Swift. I’ve been dipping my toes into iOS and MacOs development lately, which has required me to look at a decent amount of Swift code, and one of the things that jumped out is how often Swift developers use Structs in their apps. Instead of just creating a Dictionary (or hash, if you prefer) and passing that around, they’d create a Struct to name and encapsulate what that data actually represents, and pass around instances of the Struct instead. Seeing that made me realize that I had never actually seen a Struct used in any ruby apps I’ve come across or helped build, and led me to think more about value objects in general.

I should start with a brief definition of a value object. This one is from Patterns of Enterprise Application Architecture by Martin Fowler:

The key difference between reference and value objects lies in how they deal with equality. A reference object uses identity as the basis for equality—maybe the identity within the programming system, such as the built-in identity of OO programming languages, or maybe some kind of ID number, such as the primary key in a relational database. A Value Object bases its notion of equality on field values within the class. Thus, two date objects may be the same if their day, month, and year values are the same.

There are lots of other principals, such as immutability and enforcing validity, that can quickly make their way into a discussion about value objects. Those are slightly more advanced topics, but I want to focus on the baseline use case. You have some data that gets passed around in your app pretty often–maybe it’s the response from an api call, or a hash containing values from several models–and you need to give this blob of data a name. Just having a name for it can make the concept of what you are doing with it more understandable and easier to deal with. Well, these are ways you can start doing so in your app right now. Once you get more comfortable, then you can start to add on that more advanced functionality.

The Application

Let’s say you’re building an app that involves checking to see if two people are currently in the same location. To do so, first you need location information from your users. Then, based on that information, your app performs some action, like notifying someone that their friend is nearby. You’re not persisting this information anywhere, like a database, you just need to act on during the request and then throw it away. What you need is a value object of lat-long coordinates to represent each person’s location. A value object works perfectly for this–two people are in the same place if the latitude and longitude values are the same. Here are some ways to implement that in ruby.

Native Ruby ways to create Value Objects

Structs

The simplest way to create a value object in ruby is by using a Struct. Ruby Structs are very different from Swift Structs, and seem to have gotten a bad rap, but they’re still useful. They’re also very easy to create. When you’re creating the type you define the properties you’ll need, and when you initialize one you pass in values. If you need to get a little fancier, you can even define your own methods.

Coordinate = Struct.new(:latitude, :longitude)
=> Coordinate

chicago = Coordinate.new(41.8781)
=> #<struct Coordinate latitude=41.8781, longitude=nil>

# automatically provides getters
chicago.latitude
=> 41.8781       

# empty properties are set to nil
chicago.longitude
=> nil

# automatically adds the setters for properties
chicago.longitude = -87.678974
=> -87.678974

chicago.longitude
=> -87.678974

devmynd = Coordinate.new(41.912047, -87.678974)
=> #<struct Coordinate latitude=41.912047, longitude=-87.678974>

devmynd.latitude
=> 41.912047

# accessing undefined properties/methods throws an error
devmynd.address
=> NoMethodError: undefined method `address' for #<struct Coordinate latitude=41.912047, longitude=-87.678974>

# can't add new properties after the fact
devmynd.address = "2035 W. Wabansia Ave."
=> NoMethodError: undefined method `address=' for #<struct Coordinate latitude=41.912047, longitude=-87.678974>

# can output to hash
devmynd.to_h
=> {:latitude=>41.912047, :longitude=>-87.678974}

# .class method returns the type name you created
devmynd.class
=> Coordinate

# easy to determine the type later on if necessary
devmynd.class == Coordinate
=> true

hq = Coordinate.new(41.912047, -87.678974)
=> #<struct Coordinate latitude=41.912047, longitude=-87.678974>

# double equal (==) by default compares subclass, then values
devmynd == hq
=> true

# triple equal (===) still compares by reference
devmynd === hq
=> false

# can also define methods
Address = Struct.new(:company_name, :street, :city, :state, :zip) do
  def address
    "#{company_name} is located at #{street}, #{city}, #{state} #{zip}"
  end
end
=> Address

devmynd = Address.new("DevMynd Software", "2035 W. Wabansia Ave", "Chicago", "Illinois", 60612)
=> #<struct Address street="2035 W. Wabansia Ave", city="Chicago", state="Illinois", zip=60612>

devmynd.address
=> "DevMynd Software is located at 2035 W. Wabansia Ave, Chicago, Illinois 60612"

That last example shows one of the ways that ruby structs start to break down. You always need to remember the exact order in which the properties were defined. You can’t just pass in a hash. This isn’t as much of a problem when you only have two or three properties, but any more than that and you risk adding arguments in the wrong order, forgetting properties, forgetting to purposely set a property to nil, or creating other small bugs which can be annoying to find and fix.

You can also start to get pretty deep in the weeds with structs, For example, lets say you have an array of value objects and you want to be able to sort them. You’ll need to define the spaceship (<=>) operator.

# Sort Coordinates based on latitude
Coordinate = Struct.new(:latitude, :longitude) do
  def <=>(other)
    if latitude > other.latitude
      return 1
    elsif latitude < other.latitude
      return -1
    else
      return 0
    end
  end
end
=> Coordinate

here = Coordinate.new(1.2345, 2.3456)
=> #<struct Coordinate latitude=1.2345, longitude=2.3456>

there = Coordinate.new(1.2345, 3.4567)
=> #<struct Coordinate latitude=1.2345, longitude=3.4567>

# Comparing values? still what you would expect
here == there
=> false

anywhere = Coordinate.new(5.6789, 6.7891)
=> #<struct Coordinate latitude=5.6789, longitude=6.7891>

places = [anywhere, here, there]
=> [#<struct Coordinate latitude=5.6789, longitude=6.7891>, #<struct Coordinate latitude=1.2345, longitude=2.3456>, #<struct Coordinate latitude=1.2345, longitude=3.4567>]

# Now sorts an array of Coordinates based on latitude
places.sort
=> [#<struct Coordinate latitude=1.2345, longitude=2.3456>, #<struct Coordinate latitude=1.2345, longitude=3.4567>, #<struct Coordinate latitude=5.6789, longitude=6.7891>]

# What about using comparison operators?
ComparableCoordinate = Struct.new(:latitude, :longitude) do
  include Comparable
  def <=>(other)
    if latitude > other.latitude
      return 1
    elsif latitude < other.latitude
      return -1
    else
      return 0
    end
  end

  alias_method :north_of?, :>
  alias_method :south_of?, :<
end
=> ComparableCoordinate

here = ComparableCoordinate.new(1.2345, 2.3456)
=> #<struct Coordinate latitude=1.2345, longitude=2.3456>

there = ComparableCoordinate.new(1, 5)
=> #<struct Coordinate latitude=1, longitude=5>

here > there
=> true

here.south_of? there
=> false

here == there
=> false

anywhere = ComparableCoordinate.new(1.2345, 3.4567)
=> #<struct Coordinate lat=1.2345, long=3.4567>

# Careful, now that you included Comparable, == is based on how you defined <=>
here == anywhere
=> true

Honestly, at this point you’re doing a bit too much and should probably consider just using a class.

So a Struct is a good starting place, but if your value object grows to more than a couple properties, it could get unwieldy.

OpenStruct

OpenStruct is part of the Ruby Standard Library so, unlike with a struct, you’ll need to require it in the files where you’re using it. It’s flexibility may be useful in situations when you don’t know what properties you’ll need in advance, like when making an API call. You can create a object with whichever getters and setters you need by simply declaring the properties on initialization or assigning a value later. Be careful though, because a big source of errors with openstructs is simply misspelling a property when you set it. There’s no failsafe to let you know that you’ve transposed some letters.

require 'ostruct'

devmynd = OpenStruct.new(latitude: 41.912047)
=> #<OpenStruct latitude=41.912047>

# Automatically defines getters
devmynd.latitude
=> 41.912047

# Nil for any unset properties
devmynd.longitude
=> nil
devmynd.address
=> nil

# You can add any property you need
devmynd.longitude = -87.678974
=> -87.678974

devmynd
=> #<OpenStruct latitude=41.912047, longitude=-87.678974>

hq = OpenStruct.new(latitude: 41.912047, longitude: -87.678974)
=> #<OpenStruct latitude=41.912047, longitude=-87.678974>

# Automatically does value comparison
devmynd == hq
=> true

# But be careful with your spelling when you set a property
hq.lognitude = -85.12345
=> -85.12345

hq
=> #<OpenStruct latitude=41.912047, longitude=-87.678974, lognitude=-85.12345>

# Want to define methods? Use inheritance
class Coordinates < OpenStruct
  def location
    "We are located at lat: #{self.latitude}, long: #{self.longitude}."  
  end
end
=> :location

devmynd_west = Coordinates.new(latitude: 37.769847, longitude: -122.420487)
=> #<Coordinates latitude=37.769847, longitude=-122.420487>

devmynd_west.location
=> "We are located at lat: 37.769847, long: -122.420487."

devmynd_west.class
=> Coordinates

devmynd.class
=> OpenStruct

devmynd_west.is_a? OpenStruct
=> true

# You still need to have the correct properties set
chicago = Coordinates.new
=> #<Coordinates>

chicago.location
=> "We are located at lat: , long: ."

chicago.latitude = 41.881832
=> 41.881832

chicago.location
=> "We are located at lat: 41.881832, long: ."

The flexibility of OpenStruct comes at a price. There are a ton of articles about why you shouldn’t use OpenStruct for performance reasons. Apparently it uses method_missing and define_method in order to dynamically generate those getters and setters. You should read about it yourself and make up your own mind based on your needs. However, I’ll note that there are alternatives that try to mimic its functionality while also speeding it up. Many people use OpenStructs to mock out objects in the context of their tests. Performance might not be as much of an issue there, since you’ll have a fixed number of objects. YMMV.

Ruby Classes (Plain Old Ruby Objects)

I’m not sure what really needs to be said about classes. You know them, you love them, and you already use them all the time. That’s a huge advantage in its favor. The great strength of using a class for a value object is its flexibility. You define the properties, you can define the getters and setters, you can define methods to manipulate the data or customize the output—define anything you want.

class Location
  attr_reader :latitude, :longitude

  def initialize(coordinates)
    @latitude = coordinates[:latitude]
    @longitude = coordinates[:longitude]
  end
end
=> :initialize

devmynd = Location.new(latitude: 41.912047, longitude: -87.678974)
=> #<Location:0x007fff490ab388 @latitude=41.912047, @longitude=-87.678974>

hq = Location.new(latitude: 41.912047, longitude: -87.678974)
=> #<Location:0x007f9cdc892ad0 @latitude=41.912047, @longitude=-87.678974>

# not a value comparison, in a class == checks the object reference
devmynd == hq
=> false

devmynd.class
=> Location

But having to do everything yourself could get tedious. Fortunately, if you’re creating a Rails app, you can always use some of the modules from ActiveModel to help out, but you still need to redefine your equality (==) method to make your class act like a value object.

class Location
  attr_reader :latitude, :longitude

  def initialize(coordinates)
    @latitude = coordinates[:latitude]
    @longitude = coordinates[:longitude]
  end

  # ******* REDEFINING == *******
  def ==(other)
    # make it operate same as a Struct, first check type, then values
    (self.class == other.class) && (self.latitude == other.latitude) && (self.longitude == other.longitude)
  end
end
=> :==

devmynd = Location.new(latitude: 41.912047, longitude: -87.678974)
=> #<Location:0x007ffea2911038 @latitude=41.912047, @longitude=-87.678974>

hq = Location.new(latitude: 41.912047, longitude: -87.678974)
=> #<Location:0x007ffea2981b08 @latitude=41.912047, @longitude=-87.678974>

# now acts like a value object
devmynd == hq
=> true

# error for other undefined properties/methods
devmynd.address
NoMethodError: undefined method 'address' for #<Location:0x007ffea2911038>

I would actually advocate strongly against redefining the double equals operator. I think future you and the other people that might inherit your code at some point would also thank you for not doing it. At the very least it should be extremely clear that you’re doing so. But, it’s there if you want to.

Gems that provide Value Object functionality

Lastly, in case you don’t like any of those 3 options, I want to leave you with a few gems that implement data types which can be used as value objects. This is definitely not an exhaustive list (and I don’t actually like some of them) but it’s worth checking out if they might work for you. Good luck!

  • Immutable Struct: My favorite of the bunch, and likely to be my go-to gem, because of its simplicity. In very little code it fixes many of the issues with native structs. It adds immutability by removing the setters for properties, you to initialize the struct with a hash, and you can make all the properties required.
  • Hashie: Used in lots of important gems , however…
  • Dry-Struct: also requires you to implement some type-checking.
  • Virtus
  • ActiveAttrExtended
  • Values
  • Anima
Eryan is a development director in DevMynd’s software engineering practice focusing on mobile apps and web development. He has been with the company since 2013.