Avoid primitive obsession and build your own context

Primitive obsession is a known code smell that describes the usage of primitives for representing domain values. For example:

class User(
val name: String,
val age: Int
) {
init {
require(name.length in (2..25))
require(age in (0..110))
}
}

In this class both the user’s name and age are being represented by a string and an int respectively.

How else could we represent a name and an age?
Well, the primitives are the correct ones but not for direct usage. We can build value objects upon them and encapsulate both the meaning of the value and the business logic:

class User(
val name: Name,
val age: Age
)
class Name private constructor(
val value: String
) {
companion object {
fun of(value: String): Name {
require(value.length in (2..25))
return Name(value)
}
}
}
class Age private constructor(
val value: Int
) {
companion object {
fun of(value: Int): Age {
require(value in (0..110))
return Age(value)
}
}
}

So what? You just wrapped a primitive and moved the check.
True but now we have:

1. A reusable object

When the time comes and we need to have a new named entity we just use the class above and we can be sure that the business logic will follow along and be concise in the entire project.

2. Always valid instances

Whenever we see an instance of name or age we are certain that the instance holds a valid value. This means that an entity that consists from those value objects can only create valid instances and this means that the code that uses those entities (and value objects) does not need to have unnecessary checks. Cleaner code.

3. Code that scales more easily

Lets say that our business logic changes and we need to support users with invalid names too but without the need to deal with the name itself.
Having a value object can help implement the change easily. We just change the Name class. All other code remains the same:

sealed class Name constructor(
val value: String
) {
object UnknownName : Name("")
class KnownName(value: String) : Name(value)
companion object {
fun of(value: String): Name {
return if (value.length in (2..25))
KnownName(value) else
UnknownName
}
}
}
4. Code that is more robust

Lets say that our entities have the notion of an id and that after a few years the underline value needs to change from an integer to a long.
By having a value object to represent the Id all changes will take place in the outer layers of our architecture where we load/fetch ids from databases/network and create the id instances. The rest of project will remain untouched, especially the domain layer that holds our business logic.
If we had chosen the path of having an integer property in every entity then all of our entities, and the code that uses them, would need to change too.

5. Code that is more readable and reveals its usage

I’ve written a couple of posts in the past that showcase both the readability aspect and the revelation one.

6. A blueprint of our domain

When we open our project and see files like Invoice, Price, Quantity, Amount, Currency we get an immediate feel of what this project/package deals with and what are its building blocks.
We consume information that we would otherwise need to dig inside each file to find out.

7. A context

The final and most important part of having value objects is that now we can complete our entities and build a context for our domain. A common language that we can use to communicate with other engineers and stake holders in general.
Primitives are essential and they are the building blocks of a language but not of our business. The rest of company does not build its workflows upon integers and strings. It uses the businesses’ building blocks like age, name etc. It is vital that we do the same too in order to keep our code base in sync with the business.

Make your code reveal its usage

A small and simple example that shows one of the benefits of having domain objects.

Lets assume we need to make a request for some kind of a token and the flow for doing so goes like this:

  1. Validate an email address
  2. With the validated address validate a password
  3. With both the validated values request for the token

The not so revealing way

fun validateEmailAddress(value: String) {
// does some validation
}
fun validatePassword(validEmailAddress: String, password: String) {
// does some validation
}
fun requestToken(validEmailAddress: String, validPassword: String): String {
return "some kind of token"
}

Q: Can the consumer of this API, without knowing the aforementioned flow, figure it out just by looking at the functions’ signatures?

A: I guess she could if the parameters’ names were like that. If not she needs to read the bodies of those functions to see that there is some kind of order that needs to be followed.

Q: Can the creator of this code be 100% certain that the functions will be used as expected and the passed parameters will be valid? For example, will requestToken always be called last and with valid addresses and passwords?

A: No! There is nothing that can guarantee that so, just to be safe, we check in every function that the provided values are valid and make the code flexible enough that, for example, each function could be used on its own. That will, potentially, lead to code duplication or unnecessary abstractions.

Q: What about errors and invalid values? Can the consumer of this API predict the code’s behavior on erroneous inputs without reading a documentation?

A: No. Just no.

The revealing way

First lets enrich our API with some, much needed, domain objects:

sealed class EmailAddress
data class ValidEmailAddress(val value: String) : EmailAddress()
data class InvalidEmailAddress(val value: String, val error: String): EmailAddress()
sealed class Password
data class ValidPassword(val value: String) : Password()
data class InvalidPassword(val value: String, val error: String): Password()
data class Token(val value: String)

Having those constructs we can change the functions and make them reveal both the order they can be used and their behavior:

fun validateEmailAddress(value: String): EmailAddress {
// does some validation and
// if the validation fails it returns InvalidEmailAddress
// otherwise a ValidEmailAddress
}
fun validatePassword(emailAddress: ValidEmailAddress, password: String): Password {
// does some validation against the valid email address
// if the validation fails it returns an InvalidPassword
// otherwise a ValidPassword
}
fun requestToken(emailAddress: ValidEmailAddress, password: ValidPassword): Token {
// having only valid values makes the code simple
// just make the request and return the token
return Token("some kind of token")
}

So lets pretend that we are the consumer of this API and all we have is the code.

Our main goal is to request for a token. By just looking at the functions’ signatures we see that there is a requestToken. Nice! Our task is half way done! What do we need for calling this function? A valid email address and a valid password. Ok. How do we get one of each?

Looking again at the signatures we see validateEmailAddress and validatePassword that return an EmailAddress and Password respectively. Lets check what those are. These are abstractions and each of them has been extended to a valid and an invalid state. The invalid one carries the error that occurred too! So, back to the functions.

We see that validating a password needs to be fed with a valid email address so we first need to call validateEmailAddress, then validatePassword and finally requestToken.

That’s it. We didn’t read the code of the functions and we didn’t have to worry about our inputs.