Little Thing — Value Objects

Danny Smith
5 min readNov 5, 2018

This is part of my Little Things series. Although these examples are in ruby, they should be understandable to anyone familiar with an object-oriented language.

What are value objects, and when should we use them?

Let’s imagine that we have a some code that uses coordinates, and we’re defining a couple of variables:

canvas_position_x = 25
canvas_position_y = 200
draw_canvas(canvas_positionX, canvas_position_y)

Most programmers would see this and think: that’s a smell, those two variables always belong together — they should be one thing. The obvious next step is to put them in some sort of hash table. In ruby, that’s a hash:

canvas_position = {
x: 25,
y: 200
}
draw_canvas(canvas_position[:x], canvas+position[:y])

This is better, but is still not perfect. We are still using a primitive data type which doesn’t know or care that it’s holding a coordinate. Instead, we could create a value object to store our coordinates.

class Coordinate
attr_reader :x, :y
def initialize(x:, y:)
@x, @y = x, y
end
end
canvas_position = Coordinate.new(x: 25, y: 200)draw_canvas(canvas_position.x, canvas_position.y)

Advantages to this approach

  1. We cannot accidentally create a coordinate without an x or y value.
  2. We cannot accidentally modify the x or y value of a coordinate — instances of Coordinate are immutable (they have no setter methods).
  3. If we try to call canvasPosition.z we won’t get nil, as we would with a hash. We’ll get an exception telling us there’s no method z for Coordinate. This makes it easy to understand what went wrong.
  4. We have been forced to explicitly name this thing, using language specific to our business domain (in this case “Coordinate”).
  5. We can add methods to the value object. (And in our heads, their names are scoped to the object’s context. It’s fairly obvious what Coordinate#reverse does, but using just “reverse” in a more general context might be confusing).
class Coordinate  #...  def reverse
self.class.new(x: y, y: x)
end
end

For rubyists, this might all feel pretty obvious, since everything in ruby is an object. Ruby’s primitive data types like Symbol, String, Integer and Range are all value objects anyway. They also all share a property which our Coordinate objects do not…

Their Equality is based on their values.

a = Coordinate.new(x: 1, y: 2)
b = Coordinate.new(x: 1, y: 2)
a == b #=> false

One of the most important properties of a value object, when compared with other sorts of object, is how they handle equality. According to Martin Fowler

…their notion of equality isn’t based on identity, instead two value objects are equal if all their fields are equal.

So we need fix this. We can implement a == method that only returns true if the x and y values are the same as the thing we’re comparing to and if the thing we’re comparing to is also a kind of Coordinate. We should alias eql? since that’s pretty standard in ruby.

class Coordinate  #...  def ==(other)
self.class == other.class &&
x == other.x &&
y == other.y
end
alias :eql? :==
end

If two Coordinate objects are considered equal, then they should also have the same hash. Let’s override the hash method provided by BasicObject with our own, which generates the hash based on the values our object is holding:

class Coordinate#...  def hash
[@x, @y].hash
end
end
a = Coordinate.new(x: 1, y: 2)
b = Coordinate.new(X: 1, y: 2)
a == b #=> true

What about Structs, tho?

You might be thinking that a Struct would remove a lot of this boilerplate. We could get everything working — including all the equality stuff — with just one line of code:

Coordinate = Struct.new(:x, :y, keyword_init: true)

Awesome, right?

Maybe not. While structs tick almost every box, they are mutable. 😞 In other words, they define setter methods for x and y. For me, this is a dealbreaker, though you could always get round this by creating an immutable version of Struct, like this.

I tend to reach for value objects when I see…

  • A data clump: two or more values that always belong together. These are often pairs values like x and y or start_date and end_date. If you see the same values being passed through multiple methods together, it’s usually a good indicator.
  • A piece of data which has functionality. Temperature is a good example: Although it’s a single value, it might also have methods like below_freezing?. If you see a group of methods that are all operating on the same value, it’s a good indicator.

My rules for writing value objects

  1. They should be small.
  2. They must be immutable.
  3. They must implement a hash method.
  4. They must implement a == or <=> method.
  5. If their values can be ordered, they should include Comparable and implement a <=> method.
  6. If the constructor takes more than one argument, it should always use keyword arguments. Otherwise it should not.
  7. No arguments should be optional. If it sets defaults or injects dependencies, it is probably doing more than just storing a value.
  8. All mutable arguments passed to the constructor should be frozen before they are assigned to instance variables.

Summary

For me, using value objects has advantages and disadvantages. On the one hand, they can make your code easier to understand and less coupled. In ruby, you can take advantage of duck typing to make your code code more flexible. Value objects also make your code more defensive: you will likely more descriptive errors.

On the other hand, you’ll end up with a lot more classes and unit tests to maintain and the cognitive burden on people reading your code can be a bit higher. Over time, it’s also easy to inflate value objects into massive classes that do too many things (and aren’t really value objects anymore).

I’m coming round to the idea that value objects are most useful when working with the core business logic of your application: the domain layer. If you watch a few talks on Domain-Driven Design, you’ll see what I mean.

Further Reading

Bonus

If we wanted to, we could try to pull some of this functionality into a generic class and inherit from it. It might look something like this…

END

--

--

Danny Smith

Helping companies become high-performance remote organisations. Currently working with @HeyOyster . Used to write code. Occasional blues musician.