In today’s dev discussion, we first talked about what a value object is, what types of code in your application make for good extractions to value objects, and how we would implement value objects.
What is a value object?
We all agreed that the most important tenet of a value object is that equality must be based on the value the object represents not on object or database identity. This doesn’t mean you must implement the ==
method. If that’s not important to your interface, you don’t need to implement it.
We discussed the idea that value objects should be immutable. Allowing a value object to mutate is dangerous, though in Rails we tend not to worry about this at the implementation level. Personally, I tend to think of these things as immutable by agreement. We hypothesized that this might be due to the extremely short lifetime of value objects in our rails application.
@jferris’s note on the discussion topic also mentioned that value objects should not have behavior. We discussed what would actually constitute behavior. That is, if you add formatting to (to_s, to_html) to your value object, are you adding behavior? We decided that methods relying only on the value represented by the object and had no external side effects were okay for a value object.
In the classic example of a value object representing money, a value object representing $100 USD could know to print itself as $100.00
but it probably should not know how to convert itself to pesos. The value of $100 knows that it’s 100 dollars - it does not know how many pesos it is.
** What types of things make for good value objects?**
- Parameter objects - A set of parameters passed together to a method. These are easy to spot when you pass them to multiple methods, but can also make sense when a group of parameters is passed only to one method. Can you name that group?
- View Object - something you’re passing to a view for rendering.
- Money, Addresses, etc
** How do you implement a value object?**
The simplest way is probably with a struct:
Point = Struct.new(:x, :y)
Structs, however, are mutable and are allowed to be instatiated with fewer than the number of required parameters (Point.new(1)
), which sets the remaining parameters to nil. Extracting a class allows you to overcome the initialization issues and give you a convenient place to try to overcome mutation issues (you have to write the constructor, so you might as well clone there).
The values gem gives you a struct-like creation while overcoming the mutability and initialization issues. This led us to wondering why we often find ourselves unwilling to pull in small libraries such as this which seem like a clear win if you were ever planning on writing a struct. Perhaps a ruby thing? A fear of dependencies (particularly those with more dependencies)? In any event, we took a dive into all 45ish lines of the values gem and learned some interesting things that were quite out of scope for the discussion. It’s worth a look.