What are value objects
In software engineering we constantly need to work with data that has meaning.
Simple things for example like phone-numbers, email addresses, dates, etc.
However they can also be a bit more complex like a coordinate (has multiple values inside of it) or names (a name can have different rules in different countries).
Obviously we can just use primitives for these values and handle all cases in the code where we are using them.
However, that means lot’s of code that is maintained in multiple places, and you are not always sure you are getting the expected data.
That is where value objects come into play.
In short: a value object is an object that holds a value.
By using this object you should always be sure that when that object exists, that also the value exists and that the value is correct.
This is due to a few rules I keep in mind when working with value objects:
- The value passed into the value object is checked by the value object to make sure it is a valid value.
- The value object is immutable.
- The value object exposes an api to access (parts of) the data hold within.
Let me explain what I mean by that:
Rules when creating value objects
Checking validity
When passing data into a value object, the value object should check whether or not that data is in a valid format.
This means that the value object checks for example that an email address uses a format like: user@domain.tld
What it should not do is actually checking whether the email address exists. So no mx lookup for example. When you would do this, the value object would break the responsibility of being a simple object.
If you want to do more complex validations a common pattern for this would be to use the factory pattern. A factory can do more complex validations, and depend on services it needs to do them.
An example then:
public record EmailAddress(String emailAddress) {
// Regex provided by the owasp regex repository: https://owasp.org/www-community/OWASP_Validation_Regex_Repository
private static final Pattern VALIDATION_PATTERN = Pattern.compile( "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$");
// For records using a compact constructor would be cleaner.
public EmailAddress(String emailAddress) {
if(isAnInvalid(emailAddress)) {
throw new InvalidEmailException();
}
this.emailAddress = emailAddress;
}
private boolean isAnInvalid(String emailAddress) {
return null == emailAddress || !VALIDATION_PATTERN.matcher(emailAddress).matches();
}
@Override
public String toString() {
return emailAddress;
}
public String getUser() {
// We already know the email address is correct, so this method is safe to use.
return emailAddress.substring(0,"emailAddress".indexOf("@")-1);
}
}
public class EmailAddressFactory {
// I leave the actual implementation up to you. This is out of scope for this example and differs per used framework.
private final EmailCheckService emailCheckService;
public EmailAddressFactory(EmailCheckService emailCheckService) {
this.emailCheckService = emailCheckService;
}
public Optional<EmailAddress> createEmailAddress(String emailAddressString) {
try {
EmailAddress emailAddress = new EmailAddress(emailAddressString);
if (emailCheckService.emailAddressExists(emailAddress)) {
return Optional.of(emailAddress);
}
} catch(InvalidEmailException e) {
// It is an invalid email Address. The default return will handle this case.
}
return Optional.empty();
}
}
JavaThe above example is a bit too simplified.
For production code I would add meaningful information in returning data.
That way services that need the EmailAddress can handle invalid email situations.
The goal of the example is to showcase how this setup would look like.
Immutable value objects
As we do expect the value object to always have a correct value in it, we need it to be Immutable.
This means that there are no setters on the object, and when it does return a value, it should always be a copy of the value it is containing.
There are numerous benefits of Immutability, and I will not get into that right now. This will be a topic for another blog post.
Exposing an api to access the hold value
A value object should have an api to access it’s data.
Not just that, but also to access parts of it’s data.
Obviously this is part of proper OO design anyway.
However let’s get back to the example of our EmailAddress (leaving out implementations for convenience):
public record EmailAddress(String emailAddress) {
// ...
@Override
public String toString() {
return emailAddress;
}
public String getUser() {}
public String getDomain() {}
public Tld getTLD() {} // Yes a value object can use other value objects when they are part of the same package!
}
JavaThese methods do only use the information that is stored inside of the value object.
Thus as mentioned earlier a method likecheckMxRecord
should not exist in the value object. This is due to it not only using the value stored within the value object.
So far we have looked at the rather simple setup of the email address. So let’s also take a look at the Tld object:
public record Tld(String tld) {
private static final Map<String, String> ORIGIN_MAP = Map.ofEntries(
entry("com", "global"),
entry("eu", "european union"),
entry("nl", "netherlands"),
entry("io", "global")
);
// ...
public String toString() {
return tld;
}
public String getOrigin() {
return ORIGIN_MAP.get(tld);
}
}
JavaThis tld example is interesting as it does not only contain the information about the tld that got injected. It also contains static information about potential injected values.
This is good as it is static data, so does not get mutated. Also it is tightly coupled to the values it represents and it does not need anything outside of the value object.
It does in fact, held the value object with checking if the passed data is correct, and provides the user of the object a richer and safer experience when working with this data.
Conclusion
In summary value objects are awesome.
They provide an easy way to represent certain types of data in a safe way.
We know that the data is correct, and when we need to get anything about the data, we can just ask the value object for it.