Mutating nullable composites
This article is about convenient and safe mutations of composite value objects that implement the null object pattern.
Here’s an example of a composite value object. It’s a user that has two subtypes: an email address and a name.
final class User
{
private $emailAddress;
private $name; public function getEmailAddress(): EmailAddress
{
return $this->emailAddress;
} public function getName(): Name
{
return $this->name;
} public function withName(Name $name): User
{
$native = $this->toNative();
$native['name'] = $name->toNative();
return self::fromNative($native);
}
}
To mutate the name of a User
you take an existing object and call the withName
method on it. This method returns a new instance of User
where all the previous values are the same, but name
is now set to the new value.
$newName = new Name('Ben Sisko');
$user = $user->withName($newName);
Making it
nullable
If we wanted to make User
nullable, we could follow the null object pattern. First, we use an interface to define the type.
interface User
{
public function getEmailAddress(): EmailAddress;
public function getName(): Name;
}
We can then implement a non-null version of that type by adapting the User
class above.
final class NonNullUser implements User
{
...
And finally, a null implementation.
final class NullUser implements User
{
public function getEmailAddress(): EmailAddress
{
return new NullEmailAddress;
}
public function getName(): Name
{
return new NullName;
}
}
It’s always assumed that if a composite is nullable, all of its subtypes are nullable too. Because if all of a null composite’s subtypes weren’t null — it couldn’t possibly be null itself.
Mutating the null
To mutate the nullable, we need to add the mutation method to the User
public interface.
interface User
{
... public function withName(Name $name): User;
}
The non-null would implement the withName
method in the same way as before. But for the null implementation we need to do something slightly different.
final class NullUser implements User
{
... public function withName(Name $name): User
{
if ($name instanceof NullName) {
return new self;
} return NonNullUser(
new NullEmailAddress,
$name
);
}
}
If the User
is null and the caller is trying to mutate the Name
to be null — then the User
is still null. So we just return a null User
because nothing has changed. If the Name
is non-null then we create a new non-null User
where all values are null except for the new value.
This approach makes mutating nullable objects extremely convenient. Because all you have to do is $user->withName($name);
and all possibilities are handled as long as $user
and $name
conform to the correct interfaces.