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.
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.
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.
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:
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.
#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 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.
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.
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.