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.

Like what you read? Give Chris Harrison a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.