Mutating nullable composites

Chris Harrison
May 1, 2018 · 2 min read

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.

Chris Harrison

Written by

Software engineer

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade