Code Simplicity - Value Objects

May 09, 2017 | Nicolas Zermati | 4-minute read

Understanding the application’s state at a given point in time is valuable. You and your team must make efforts to keep the cognitive load required to reason about its state as low as possible.

Application’s state is often based on classes such as Numeric, String, Array, etc. In this article we’ll see how to abstract business-specific objects on top of those primitive types.

A simple specification

I need to model a car. A car is simply defined by its serial number and its mileage. In addition to this, a car will have an interface to update distance that have been driven. When a car is created the serial number is generated and the mileage is set to zero.

class Car
  def initialize
    @serial_number = SerialNumber.generate(self)
    @mileage = 0
  end

  def drive(distance)
    @mileage += distance
  end
end

Here I used another module to generate the serial number. It isn’t the purpose of the article so let’s ignore it for this time. To express the distance, I used a Numeric instance. Indeed, nothing had been said explicitly in the specification.

Fighting implicitness

Here I implicitly expect the distance passed to the drive method to be positive. It obiously is because a negative distance make no sense!

However, something looking obvious now to you might not look the same to someone else or in the future. A code with a lot of implicit constraints is hard to trust because for each change you’ll have to carry in your head all those implicit constraints and make sure they are still enforced. I don’t know about you but this looks scary as hell to me.

There is different way of fighting this implicitness. We could try to add safeties to our code to mitigate the unexpected inputs. It would look like this:

def drive(distance)
  @mileage += distance.abs
end

Tada! No more problem having a negative number as argument!

This is, in my opinion, worse than the first version. Now there is some misplaced code in the Car class. It raises questions that makes no sense.

  • Why a distance would be negative?
  • Is that #abs call really needed?

Those are hard questions, especially when it isn’t your code, when it is in a critical part of the application, and it has been there forever. Those questions are hard because some would find obvious that a distance must always be positive.

Other programming environments helps you express that kind of constraints using advanced type systems. Ruby, on the other hand, is more permissive and the responsibility of making things explicit, relies on the design you’ll come with.

The right battlefield

The issue is that we have no place to express that implicit constraint about the distance being positive. The car shouldn’t be responsible to manage this. Lets fight the distance battle on a more appropriate battlefield: in a Distance class.

class Distance
  def initialize(value)
    if value < 0
      raise ArgumentError, "A distance must be positive"
    end

    @value = value
  end

  def +(other)
    unless other.kind_of?(Distance)
      raise ArgumentError, "Only another distance can be added"
    end

    Distance.new(value + other.value)
  end

  attr_reader :value
  protected :value
end

class Car
  def initialize
    @serial_number = SerialNumber.generate(self)
    @mileage = Distance.new(0)
  end

  def drive(distance)
    @mileage += distance
  end
end

This Distance class isn’t perfect but the Car class is more robust than it was and even more expressive. Distance is a value object that we created in response to a primitive obsession code smell.

In this example, we made the concept of a distance explicit. It allowed us to express the constraints related to the concept itself.

One could argue that it was shorter with the implicit version. It was shorter to write. Code is read way more often than written. Once the distance class is done, no need to read it each time you use it. And finally, if you only look at the Car class, the last version express more and is safer.

Going further

Value objects are not only good for giving a home to implicit constraints. They are also good to aggregate things that belong together. For instance, an amount of money will need a currency and an amount. A value object can tie them together and prevent operations mixing currencies.

Internet is full of articles about value objects! Read them all as each of them would give you a different perspective on this topic.

View openings 👍  Like this post? Join Drivy's engineering team!