Overcoming Primitive Obsession
Ruby primitives are so powerful that we use them everywhere in our code, even when we shouldn't. How many times we express a path or a URL with String
instead of Pathname
and URI
?
path = "path/to/file.rb"
url = "https://dnsimple.com"
# vs
require "uri"
require "pathname"
path = Pathname.new("path/to/file.rb")
url = URI.parse("https://dnsimple.com")
Ruby literals are handy to use and we don't recognize this scheme anymore. Until these are just values to pass around the two versions of the code above are more or less equivalent. But we can run soon into problems when we need to perform operations on these values. Does the file exist? Is the URI wellformed? Does it use HTTPS? These are questions that String
can't answer.
This problem is known as Primitive Obsession.
A Problem In Our Codebase
In a recent refactoring, I spotted a constant in our code:
REGIONS = [
['California, US', 'SV1'],
['Illinois, US', 'ORD'],
['Virginia, US', 'IAD'],
['Amsterdam, NL', 'AMS'],
['Tokyo, JP', 'TKO'],
]
We used an array of arrays to express a domain concept: a region is a DNSimple data center. The first value in the array is a name and the second one is a code. Again, this usage is handy, but then we soon payed the price of obscure code scattered across the application:
REGIONS.map(&:last)
This "innocent" line above hides a few maintenance problems:
- It requires a mental effort to understand the intent.
- The reader must know the exact structure of
REGIONS
. - The code has to change if we change
REGIONS
structure. What if we use aHash
instead?
The Solution
To solve this problem, there are just a few steps to follow:
- Extract a class.
Region
, in our case. - Copy
REGIONS
inside the class. - Expose meaningful methods like
Region.all
. - Change the references in the code base to use to
Region
instead ofREGIONS
. - Delete
REGIONS
.
Now we have:
Region.codes
This refactored code solves a few problems:
- The code is easier to understand.
- The data structure is encapsulated by
Region
. - If the data structure changes, the surrounding code (aka caller) doesn't need to change.
Conclusion
Once Region
was extracted, I delegated to it the responsibility to validate user input (via Region.filter(data)
) and I wrote an extensive set of unit tests to cover common and edge cases.
When we use proper model domain types we improve readability, robustness (via extra tests) and new functionalities has a obvious place where to be implemented.
Luca Guidi
Former astronaut, soccer player, superhero. All at the age of 10. For some reason now I write code.
We think domain management should be easy.
That's why we continue building DNSimple.
4.3 out of 5 stars.
Based on Trustpilot.com and G2.com reviews.