Code Simplicity - Value Objects
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
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
Numeric instance. Indeed, nothing had been said explicitly in the
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
#abscall 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
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
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
Car class, the last version express more and is safer.
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.