Domain vs. Technical
Domain-Centric Code & Tests Are More Understandable
Originally published on . Last updated on .
I was recently reminded (thanks to Jon Reid @qcoding) of a tweet I wrote back in July 2020:
When doing #TDD and writing unit tests, I try to write my expectations in the language of the domain (Ubiquitous Language from DDD) instead of primitives and collections. E.g., “dice roll contains four of the same kind” and not “list has four integers of the same value”.
— Ted M. Young #BlackLivesMatter #HexArch (@jitterted) July 9, 2020
At the time, I had just started teaching my new online class[1], where this came up in a somewhat unexpected way. In the class, we’re working on fixing some Code Smells in the code of a console-based Blackjack card game. As part of the refactoring process, we extract code that compares the value of two Hand
s—the player’s and the dealer’s—to see if one of them Beats the other. We do the same for another method that compares the hands to see if they have the same value[2], called a Push.
When extracting these methods, the temptation is to name these methods with TECHNICAL terminology, i.e., using compareTo()
for the comparison method, and equals()
for the one checking for hands having the same value. However, there are two problems with these names:
-
compareTo()
has a specific usage in Java: for sorting, or with data structures that need ordering (such asTreeMap
). SinceHand
instances have no need to be ordered, usingcompareTo()
would be misleading to readers of the code. Similarly,equals()
has a specific technical meaning, but it would also be incorrect to say that two differentHand
instances are equal if they happen to have the same total value of their cards. -
If we’re defining public methods on a
Hand
class (a class concerned with an aspect of the Blackjack domain) that forms its API (what I call its “surface area”), those methods need to “speak” in the language of Blackjack.compareTo()
andequals()
are TECHNICAL names, so instead we prefer to use DOMAIN terms in the names:beats()
andpushes()
.
Benefits
The benefit of using DOMAIN terms is that it reinforces that the class implements domain behavior. It also makes the code easier to understand, even for non-coders. For me, the biggest benefit is that it helps keep the public API of the class at the appropriate level of abstraction. So, my tests are testing DOMAIN behavior, not TECHNICAL behavior. This allows the code to be more easily refactored, as domain terms are often at a higher level of abstraction than technical terms, and less prone to arbitrary change.
Example
Here, the first example uses a TECHNICAL method, contains()
, which works, but asking (querying) the Hand
whether it hasAce()
is much clearer. If you do have a DOMAIN need to find out if a Hand
has an arbitrary Card
, you might want to look deeper at the domain meaning of such a query.
Domain Knowledge
All this assumes that, as a developer, you are familiar with the domain you’re working in. If it’s a Blackjack game, you should know the terms “busted”, “pushes”, “hit”, “surrender”, etc. If it’s an accounting system, you’ll need to know the meaning of “credit”, “debit”, “journal”, “ledger”, etc. While you can write code without much knowledge of the domain, the more you deeply understand it, and the better you can work with those who define what the system needs to do.
What Do You Think?
What domains do you work in? Does your code reflect the domain, or are methods more technical? How does that affect the testability of the code or the “brittleness” of the tests? Let me know on Twitter or join my Discord to discuss this and other topics.
Now called Refactoring to Testable Code. Subscribe to my newsletter, so you don’t miss future classes. ↩︎
In Blackjack, each hand has a numeric value based on the cards it has. For details, see the rules from my Blackjack game code that I use in my course. ↩︎