by Eric Evans
Perfectionism is a pitfall. For those who, like me, love elegant design and see its utility, there is a slippery slope waiting when a satisfying design does not emerge in a reasonable time. It takes constant self-discipline to recognize this and accept, let go, ship and move on – or return in a later cycle, if it matters. It doesn’t always matter. For many parts of our systems, a good-enough design is good enough. And for those few strategically important parts, where an incisive model and a crisp design are actually likely to affect the outcome of the project, a great initial design doesn’t usually emerge immediately. We’ll need iteration and experimentation. None of our goals is furthered by grinding and dawdling, waiting for a design we can be proud of in version one.
For my own part, I try to go a bit beyond acceptance. When I am struggling with some part of the design, I often make design choices that explicitly responds the design problems themselves. This helps me let go and move on, and it can improve the chance for change in a later iteration.
In this short series I’ll describe a few techniques I use that help me take the step of shipping something I’m not happy with while also potentially leaving a good path for improvement in future iterations.
We’ll start the series with a look at very fine-grained design decisions.
Part 1: Honest Names
An honest name isn’t always an intention-revealing name. That is only the case when we succeed in fulfilling our intention with a design element. But this does not always happen. In this article, we are considering those unfortunate times when the nice solution doesn’t come.
I need a specific example to illustrate the approach. It would be easy to pick apart some poorly designed software, but it is much more interesting to examine the imperfections in something that is generally well-designed. JodaTime is a Java library for date/time, very widely used as an open source library and eventually adopted, in a modified form, as a standard library for Java. It is well-designed – but not perfect, of course. (After all, it shipped!)
Consider ‘Plus’
The JodaTime library allows us to declare times, such as a date, and periods of time:t = 2021-01-20
p = 3 days
For many applications, the storage of such values is itself an important capability. Of course, there are many common time related operations, and the library also implements some of those, such as:t.plus(p)
2021-01-20.plus(3 days) ⇒ 2021-01-23
Nice!
In JodaTime, all the values are designed as value objects. There are no mutations or side-effects. So in the example above, after executing t.plus(p), the value of t is unchanged: 2021-01-20, and the operation has returned a new value of 2021-01-23.
The ‘plus’ operation is versatile. It works seamlessly with different kinds of periods:2021-01-20.plus(2 days) ⇒ 2021-01-22
2021-01-20.plus(2 months) ⇒ 2021-03-20
The code seems very expressive and clear. And yet…
What do you think the following would return?
2021-01-30.plus(1 month) ⇒ ?
Remember, February is a short month! I can think of at least three plausible responses. To know what it actually does, I ran the code:
2021-01-30.plus(1 month) ⇒ 2021-02-28
This seems like a reasonable default behavior, but it is not obvious. The fact that I had to run the code (or look up a document somewhere) to know what would happen, even after some experience using the operation, highlights a flaw in the model. But what is wrong with it, and what could be done?
In this article, I’ll focus on the name. The word ‘plus’ seems to fit what is happening in my first few examples, but it gives no hint about how we handle irregularities, such as months of different lengths.
And this brings us to “Honest Names”. A simple name like ‘plus’ can actually be misleading if the behavior of the operation is complicated. What could it be called instead?
Exploring many options for the name of an important thing is usually a good practice. Let’s try a few:
t.plusRounded(p)
t.constrainedPlus(p)
t.plusWithCeiling(p)
t.monthAwarePlus(p)
Any of these would have at least given me a clue. They are also awkward!
We don’t like awkward designs. But we need honest names.
Give Awkward Names to Awkward Concepts
Let’s look some more at the ‘plus’ operation in JodaTime, and why ‘plus’ is a confusing name. (Here I’ll just write ‘plus’ as ‘+’ for easier reading.)
2021-03-30 + 1 month ⇒ 2021-04-30
2021-03-31 + 1 month ⇒ 2021-04-30
Should those two expressions actually have the same value?
Take a guess at the value of these two expressions:2021-03-31 + 2 months ⇒ ?
2021-03-31 + 1 month + 1 month ⇒ ?
Does it seem like the answer should be the same?
Here are the actual answers:2021-03-31 + 2 months ⇒ 2021-05-31
2021-03-31 + 1 month + 1 month ⇒ 2021-05-30
Periods can also be combined with ‘plus’, so1 month + 1 month ⇒ 2 months
And therefore,2021-03-31 + 1 month + 1 month ⇒ 2021-05-30
2021-03-31 + (1 month + 1 month) ⇒ 2021-05-31
And here we come to an insight about what is wrong with calling this ‘plus’: The name has connotations that do not apply in our context! In math, the ‘plus’ operation has properties such as associativity:(a + b) + c = a + (b + c)
When we use this name, we hint to the reader (the programmer using the library or reading code that uses it) that this operation is associative. But it is not! Lots of well defined operations are not associative. But should we call them ‘plus’?
By the way, I like to keep adding to my list of possible alternative names for a thing. Here are a few more that occur to me at this point:
t.plusRounded(p)
t.constrainedPlus(p)
t.plusWithCeiling(p)
t.monthAwarePlus(p)
t.plusNonAssociative(p)
t.plusIsh(p)
We must ship! The JodaTime designers may have noticed this issue at the time. (I don’t know anything about the inside of that project.) If they did notice, they still made the right call, in my opinion. They shipped a very useful and generally quite clean library — that was not perfect. They must have made similar decisions many times. Some issues they noticed and some they did not.
However, in a case when you have decided to ship something with awkward bits that you recognize, consider giving it an honest, awkward name. This can communicate to future users, and can help future iterations of the design.
A clean name shuts down our thinking.t.plus(p) ⇒ t
(Very soothing…)
An awkward name provokes new ideas.t.plusIshRoundCeiling(p) ⇒ t
(Please, please! My code is so ugly!)
It niggles in the backs of our minds, potentially through later iterations of the software. And meanwhile, it will communicate a realistic expectation to the user of the code.
Coming in Part 2: Refinement in a Later Iteration
Accepting a flaw in our design doesn’t mean we forget about it! In part two, we’ll take a quick look at an alternative concept that might emerge in a later iteration to relieve the awkwardness.