Primitive Obsession is a code smell.
Code smells indicate potential problems or a deviation from good programming practices. They suggest a need for investigation and possibly some refactoring of the code.
Where there's smoke, there's fire.
If you see smoke, there's likely a fire somewhere, and it definitely warrants investigation. The same principle applies to code smells.
In this article, we'll explore:
This article became a video!
Primitives are the built-in types within a programming language, such as: bool, int, string, float, array, etc.
Obsession is an extreme or unhealthy fixation on something, leading to potential harm.
Kinda like Homer with donuts!
Therefore, you could describe Primitive Obsession not only as an excessive use of primitive types, but also as an unhealthy dependency that negatively impacts your codebase.
Primitive Obsession can infiltrate our code for various reasons. Sometimes it’s a deliberate choice after evaluating trade-offs, but often it can be accidental.
As our species evolved, our bodies developed ways to save energy. Our brains developed shortcuts they can take when they need to think. One type of shortcut is called Cognitive Bias - an influence on our decision making based on personal views and opinions.
A form of Cognitive Bias is the Law of the Instrument (also known as Law of the Hammer, Maslow’s Hammer or the Golden Hammer). It’s a Cognitive Bias which illustrates our tendencies to have an over-reliance on familiar tools.
You may have heard of it with the phrase:
If the only tool you have is a hammer, it is tempting to treat everything as if it were a nail.
This is our brain taking a shortcut. We prefer to reach for what is familiar and comfortable - even if it might not be the best tool for the job.
When we are coding, our Cognitive Bias can manifest as Primitive Obsession. It’s like always using a hammer — sticking to basic data types when a better solution would be to model a new, more-specific type.
So far, we've looked at Primitive Obsession as the habit of heavily relying on simple data types to express more complex ideas.
However, the true power of understanding a code smell is to recognise the symptoms.
Just like how Design Patterns give us reusable solutions to common problems, code smells are recognisable symptoms of common issues.
Let’s do exactly that: examine some code and identify the signs of Primitive Obsession.
When our code heavily relies on basic data types, it's easy to accidentally mix up the order of arguments.
Imagine the following class:
We can create an instance of this class like this:
However, since both arguments are strings, we could:
Both arguments have the same type, allowing the order to be easily mixed up without any type violations.
The value '[email protected]' is a valid string.
The value 'abc123' is also a valid string.
If you find yourself mixing up the order of arguments, especially when the same data type represents different ideas, it might be a sign of Primitive Obsession.
We may need to implement additional email address validation in our software and consider adding some checks.
However, primitive types are unable to encapsulate behaviour. Because there is no way to place behaviour inside a primitive type — it is forced to exist outside:
The lack of encapsulation prevents us from establishing a single source of truth.
Instead of encapsulating rules and constraints within a specific type representing the concept, they are scattered throughout the codebase whenever the concept is required.
Code duplication increases the risk of errors and mistakes by hindering maintainability.
Functionality spread across a codebase is challenging to update or modify because there is no single source of truth.
Recognizing the symptoms of Primitive Obsession is a good starting point.
However, before we discuss solutions, let's dig deeper deeper to understand what these symptoms indicate because the drawbacks of Primitive Obsession aren't always obvious.
When a concept is represented using a primitive type, we are accepting all possible values the type supports.
We must consider the entire range of possible values for the primitive type and the valid values for our business domain.
When we define $email
as a string, we permit:
We must ask ourselves:
Are all valid strings also valid email addresses?
Since using a string would mean our system supported the invalid values above, it's evident it's not suitable to represent our concept.
Therefore, using a string to represent $email
would introduce Primitive Obsession.
Since code smells indicate a specific underlying issue, are there solutions for each type of code smell?
Yes!
Value Objects resolve Primitive Obsession.
Our focus isn't on Value Objects themselves, but on how to use them to tackle Primitive Obsession. If you're unfamiliar with Value Objects, I've added a list of resources at the end of this section.
Value Objects help avoid issues related to parameter and argument ordering by removing type ambiguity.
Previously, both $userId
and $email
were typed as a string, making it possible to accidentally pass one as the other. This is no longer the case. Now, in order to successfully create an instance of User
, we must provide a valid instance of a UserId
and an EmailAddress
.
We can also rely on these Value Objects. They are dependable because they incorporate business rules. If the rules were violated, their construction would have failed.
Value Objects resolve our problem with behavior existing outside of our type by encapsulating related behavior and data together.
This advantage over primitive types not only gives us a place for related behavior, but also helps us protect the creation of the type.
We can now ensure that our value enforces our business rules and that the type is consistently valid.
When we encapsulate state with behavior, as demonstrated above, we establish a Single Source of Truth. The EmailAddress
class has a single responsibility: to represent a valid email address.
Value Objects eliminate the need to duplicate logic each time a type is used. It enables us to consolidate logic related to the type within a single reusable class, promoting consistency and reducing code repetition across the codebase.
Introducing Value Objects also reduces Feature Envy (another code smell) by applying the Tell, Don’t Ask principle. If you'd like to know more about this, be sure to subscribe to my newsletter below.
An EmailAddress
type can be quite simplistic. But where Value Objects can shine is their ability to represent complex types containing multiple pieces of data.
An Address
typically contains a street, city, state, postal code, and country.
A TimeInterval
might represent a period of time and contain attributes for start time and end time.
A GeoLocation
could contain latitude and longitude coordinates.
A PersonName
might contain attributes for first name, middle name, and last name.
A CreditCard
could contain attributes such as card number, cardholder name, expiration date, and CVV.
You didn’t think you were going to read a post related to types and Value Objects without an example with Money
, did you?