Primitive Obsession — A Code Smell that Hurts People the Most
Most of the time, developers are aware of common code imperfections that are easy to recognize and know how to deal with them (through a long method, large class, a long list of parameters, duplicated code or code with a lot of comments) However, there are some code smells (imperfections) that most developers can’t detect intuitively
So in this article, I’ll focus on the primitive obsession code smell, how to recognize it, why it’s important to handle it and also describe different ways of dealing with it.
Let’s Start with the Basics!
Before jumping into the primitive obsession, let me explain what primitive fields are. Primitive fields are basic built-in building blocks of a language. They’re usually typed as int, string or constants etc.
Primitive Obsession is when the code relies too much on primitives. It means that a primitive value controls the logic in a class and this value is not type safe. Therefore, primitive obsession is when you have a bad practice of using primitive types to represent an object in a domain.
For example, lets think of representing a website’s URL. You normally store it as a String. But unfortunately a URL has more information and specific properties compared to a String (e.g. the scheme, query parameters, protocol). And by storing it as a String you can no longer access these URL-specific items (the domain concepts) without additional code.
Let me elaborate with more examples.
Examples of Primitive Obsession
I am pretty sure that as a developer, you have worked on a lot of authentication and registration module. Typically it has few requirement such as:
- User Signup, Authentication and Authorization API and so on…
As you can see, this is pretty common in most applications. Central entity in this application is User and we have a UserService to expose its API. Now we will have 2 main methods that I want to focus on.
The first one is:
public boolean authenticate(String userName, String password) {
...
}
Even before the authenticate() method gets called, it could have basic validations like
- Non-empty password,
- Length and characters combinations of the password
- and so on …
Some of these checks you might put in a separate method on a PasswordUtil class and others in StringUtil class. Also, you will have a function in HashUtil class which will be responsible for generating a hash for given password so that we can compare the 2 hashes for verification and return the result.
The second method is:
public User create(final UserDTO userDTO) {
...
}
Before the create() method is called, we validate all the fields inside the UserDTO. During this validation, we do the exact same thing on the password as we did before the authenticate method. If all the fields are valid, then within the create method, we can ensure no one else has the same user id. Then we take the raw text password and hash it so that it can be stored in the DB.
Now, I am sure you can see few of the pain points:
- There are data duplication in multiple places.
- It is not easy to guess where I can find some logic (password logic seems to be sprayed all over the place).
- How do you make sure that the password value is not accidentally modified at a later time?
- What if a new developer on the team doesn’t know about the appropriate method to use?
This is a perfect example of Primitive Obsession.
Here we are representing our domain concept password with a primitive String which is causing many issues for us. There are enough points to say that primitives are not good enough for modelling objects in your domain.
Now let’s come to the point and understand how to solve it.
It can be addressed by creating a Value Object. A Value Object represents a typed value in your domain. It is the Password, in this case.
It has three fundamental characteristics: immutability, value equality and self-validation. Let’s dive deeper into each of these three aspects:
Immutability
A Value Object should be immutable as given below.
final class Password {
private String value;public Password(String value) {
this.value = value;
}
}
But why? Here is an example.
var kg = new Weight(60);
Jim.Weight = kg;
Jack.Weight = kg;
So let’s say you have Weight as value object and assigned 60 as its value. Now you have 2 persons, Jim and Jack. Initially they have equal weight. But after some time Jim gains some amount of weight — 80
Jim.Weight.Value = 80;
But it doesn’t mean Jack has gained weight. If the value for Jim changes, so would it for Jack. Ideally that should not happen.
Also, you can share any Value Object by reference because, being immutable, it won’t be modified in another part of the code. This is very important especially if your code will be running in a multi-threaded environment.
Value Equality
Two Value Objects with the same internal values are considered equal. This means you can test for equality by checking if their internal values are all equal, respectively.
Self Validation
A Value Object only accepts values which make sense in its context. This means you are not allowed to create a Value Object with invalid values.
final class Password {
private String value;public Password(String value) {
if(value.length() < 6){
throw new InvalidLength(value);
}
this.value = value;
}
}
So If we can create a Password as a value object, then:
- Password class constructor can do the validations. But keep in mind that the constructor must not do all validations, except for some basic sanity checking shared by all context.
- You can give it another password and ask if they match. This will hide all the hashing and rehashing logic from you.
- You can kill all those 3 Utils classes (PasswordUtil, StringUtil, HashUtil) & move the logic in the Password class where it belongs.
- When Relationship logic extends, it will be placed in one place that’s dedicated to it.
So once we are done, we have the following method signatures:
public User findUser(UserId id, Password password) {
...
}
public User create(final User newUser) {
...
}
Start Thinking as Value Objects
In the beginning, it is hard to see Value Objects in your domain, but then there is an easy trick that you can follow.
- You have special validation logic that has to be applied everywhere the type of information is used (e.g. phone numbers, email addresses).
- You have special formatting logic that is used to render for humans to read (e.g. IP4 and IP6 addresses)
- You have a set of functions that all relate to the object type (like start date and end date, first name and last name), and try to model them as Value Objects.
- You have special rules for comparing similar value types.
Above are not the basic criteria to select a property as a Value Object, that can varies as per application. Sadly, this is applicable in most scenarios.
Once you start using more and more Value Objects, you will naturally get good at it. You soon be able to start to see more of this in your domain.
Conclusion
Something that makes primitive obsession interesting for me its obvious lack of popularity as a pain point. Depending on the primitive, a method that depends on a string feels less heavier than a method that is dependent on the a complex object that you’ve built yourself. But when you’re depending on a particular class, you may, in fact, be depending on a whole series of classes or tree of classes.
In my opinion, the problem arises when coupling to a primitive rather than a complex object for which you haven’t actually eliminated dependency, hidden it and made it implicit rather than explicit.
If you think of a typed language, you cannot ensure whether you’ve satisfied all the dependencies from a type perspective. As long as you pass it as a string or an int, for example, the compiler or the interpreter would say that it’s okay. It could also be that your string or int in this particular scenario wasn’t actually a valid value and this is where the key lies. So again think about the domain of possible values or the range of values that are allowed for an object or for an instance of a type at any given time.
Basically, if you have a bunch of rules for how a value is to be treated that you want to be used consistently throughout your project, create a class. If you can live with simple values because it’s not really critical to functionality, use a primitive.
Hope you will like my article., If Yes, Please like and share it so it can reach to the larger audience.