« 2014 Book Reviews
» Long-Term Travel Gear List
Code: domain-driven design, immutability, Ruby, testing, validity
I was reading Corey Haines’ book Understanding the Four Rules of Simple Design (capsule review on the 2014 book reviews post) when I read:
In fact, over time I’ve developed a guideline for myself that external callers can’t actually use the base constructor for an object. Put another way: the outside world can’t use
newto instantiate an object with an expectation of a specific state. Instead, there must be an explicitly named builder method on the class to create an object in a specific, valid state.
This was a brief aside at the tail of a longer discussion of interdependent and redundant tests. It really caught my attention and I’d like to hear it more thoroughly investigated. In the hopes of attracting Corey’s attention, I offer this cat pic and an exploration of the benefits of a similar practice I have:
As I’ve been working on Chibrary (my mailing list archive site), I’ve noticed that I’ve gotten in the habit of providing a base constructor that takes only Ruby standard library objects (
Array, etc.) and builder methods on the class to instantiate objects from other domain objects.
For example, a
MonthCount is a small value object that counts the number of discussion threads and messages in a month. The
initialize constructor takes two integers, and there is a
MonthCount.from_threadset to instantiate from a
ThreadSet (a collection of discussion threads).
When I started this habit, I was thinking of it in terms of type casting. I was creating class methods to map between my domain objects. But as I reflect on it, I’ve seen two more reasons in favor of this practice.
First, it ensures I’m building from Values rather than Entities, to use the terms from Domain-Driven Design. A Value is an data object, any two instances are interchangeable if their attributes are identical. They are almost certainly immutable and may be implemented as flyweights. Entities have identity, a continuity over time — a
Person is the same object even if they change their address, name, email address, and every other attribute on record.
It’s odd to build an object from a mutable thing like an Entity. I’ve been steadily increasing the difference between Values and Entities in this system, and constructor vs. builder is one more way.
I recognized one more reason behind my habit when rereading Sandi Metz’s Practical Object Oriented Design in Ruby. It makes my objects depend on things that change much less frequently than my domain objects. This is immediately apparent in the ease of unit testing my objects in isolation.
Constructors that only take primitives do risk creating data clumps, but as I identify them I’m comfortable relying on my own Value object, as those also change less frequently than an Entity. That dependency is neatly and explicitly encapsulated in the class method builder that maps to stdlib objects or values.
One other risk is that the constructor for an aggregate or similarly complex object may call for an ad-hoc data structure to be passed in. Every time I find myself nesting arrays and/or hashes in each other I instead use a collection of Value objects, lest other parts of the code depend on that complexity.
Where I’ve come closest to what Corey describes is my strict rule that objects may not be instantiated in an invalid state. I mentioned this last week in my post on two designs: that even if it’s not externally visible there’s an unreasonable burden in every part of a class knowing about the possibility of invalid state.
If a Value needs to be instantiated with incomplete or contradictory state, the builder methods allow building valid objects and rebuilding valid objects into new ones with the added or changed information. (This is similar to Gary Bernhardt’s Faux-O, described in his Boundaries talk.) Entities are a little more flexible because of their mutability, so they’re more likely to have updating accessors — but an Entity is still not allowed to move through an invalid state.
I can only hope Corey will chime in, but in the meantime I’m happy to hear more thoughts about builder methods from anyone reading — I’m delighted to learn that I’m not the only one who likes them.