Null, pra que te quero?

por Alan Willms

Close de tábuas no que parece ser um deque à beira de um lago

O Null Pattern, também conhecido como Active Nothing, é um padrão onde criamos classes para representar a ausência de valor, ou seja, utilizamos objetos ao invés de null.

Antes de discutirmos esse padrão, precisamos entender o que há de errado com passar e retornar null em nossos códigos.

Qual é o problema do null?

Toda vez que passamos ou retornamos null, precisamos espalhar vários testes pelo nosso sistema para checar se o valor contém alguma coisa antes de ser utilizado:

public function registrarItem($item)
{
    if (!is_null($item)) {
        $itemRepository = $this->getItemRepository();
        if (!is_null($itemRepository)) {
            $repository->persist($item);
        }
    }
}

Isso aumenta consideravelmente a quantidade de trabalho para manter um sistema, pois em todos os lugares onde usamos ou aceitamos null, precisamos checar pelo valor recebido como um argumento ou retornado por um método.

Além disso, ele torna nosso código muito frágil, pois qualquer null introduzido em um local inesperado pode causar erros do tipo “call to a member function foo() on null”, ou até mesmo problemas que podem passar despercebidos, como assumir o número zero ao invés de null em um cálculo.

Sir Tony Hoare, a pessoa responsável por introduzir o conceito de ponteiros nulos em 1965, se desculpa por ter feito isso dizendo:

“Eu chamo isso de meu erro de um bilhão de dólares. […] Na época, eu estava projetando o primeiro sistema de tipos para referências em uma linguagem orientada a objetos (ALGOL W). Meu objetivo era garantir que todas as referências fossem absolutamente seguras, com a checagem realizada automaticamente pelo compilador. No entanto, eu não pude resistir à tentação de colocar uma referência nula, simplesmente porque era fácil demais de implementar. Isso levou a incontáveis erros, vulnerabilidades e quedas de sistemas, que provavelmente causaram um bilhão de dólares de dor e danos nos últimos quarenta anos.”

Testes não resolvem o problema?

Hoje em dia costumamos escrever testes automatizados, o que diminui a possibilidade de introduzir erros por causa do null, mas ainda somos obrigados a poluir o código com essas checagens de is_null, isset, empty, etc.

As checagens por null se multiplicam. Toda vez que você chama aquele mesmo método que retorna null, você precisa fazer as mesmas verificações (if is null…) para evitar que o sistema quebre.

Além disso, você precisa escrever ainda mais testes unitários para cobrir os casos com o valor null.

PHP 7 com tipagem rigorosa

No PHP 7, se quisermos definir os tipos de retornos dos métodos e ativar a flag strict types para checar os tipos rigorosamente, não conseguimos retornar null no lugar de outro tipo.

Por exemplo, no código abaixo, o método getUser() só pode retornar uma instância de User. O valor null não é aceito como um retorno válido. Se você tentar retornar null, o PHP lança uma exceção do tipo TypeError:

declare(strict_types=1);  
public function getUser() : User
{
    // ... retorna User se houver, senão:
    return null;
}
$objeto->getUser();
# => Uncaught TypeError: Return value of getUser() must be an instance of User, null returned

(O PHP 7.1 introduziu a possibilidade de tipos nuláveis, então este não é mais necessariamente o caso)

Veremos três alternativas bastante difundidas para resolver os problemas do null: falhar rapidamente e os padrões do objeto nulo e do “talvez”.

1. Falhar Rapidamente

Falhar rapidamente é lançar uma exceção assim que for detectado um valor null, tratando essa exceção no cliente que invoca o método:

declare(strict_types=1);

class UserException extends Exception { }

class Application
{
    ...
    public function getUser() : User
    {
        if (empty($this->identity)) {
            throw new UserException('User is not authenticated.');
        }
        return $this->identity;
    }
}
try {
    echo "Olá, " . $app->getUser()->name, "!";
} catch (UserException $e) {
    echo "Olá!";
}

Essa maneira tem a vantagem de ser mais explícita, é muito mais fácil de encontrar o erro com a mensagem “User is not authenticated” do que com “Uncaught TypeError: Return value of getUser() must be an instance of User, null returned”.

Infelizmente, continuamos tendo a mesma quantidade de trabalho ou mais, pois só trocamos nossas checagens de is_null para blocos de try/catch. Essa é a melhor alternativa quando realmente precisamos do valor, quando não faz sentido prosseguir com a execução do código com um “valor provisório”.

2. Padrão do “Talvez”

Outra alternativa é criar ou utilizar uma biblioteca que implemente o Maybe Pattern. A ideia deste padrão é encapsular o acesso ao método ou propriedade em um objeto do tipo “talvez”. Esse objeto retorna o valor do método ou propriedade somente se não for null, caso contrário, retorna o valor pré-determinado.

Isso soa um pouco complexo, mas é muito simples, considere o exemplo abaixo, que utiliza a biblioteca pirminis/maybe-monad:

$loginUsuario = Maybe($usuario)->getLogin()->val('anônimo');
// se $usuario é um objeto, $loginUsuario recebe o valor de getLogin()
// se $usuario é null, $loginUsuario recebe "anônimo"
// O código acima é equivalente a:
$loginUsuario = $usuario ? $usuario->getLogin() : 'anônimo';

O problema deste método é que ele é bastante verboso e continuamos espalhando os testes por null em todo o nosso código, pois sempre precisamos chamar ->val() para informar o retorno alternativo.

Você poderia criar sua própria biblioteca que implementa este padrão e centralizar o valor alternativo quando um objeto é null, mas isso se aproximaria muito da nossa terceira alternativa, porém com mais trabalho.

3. Padrão do Objeto Nulo

A ideia deste padrão é que nada é alguma coisa. Precisamos representar essa ausência de valor de alguma forma mais significativa, criando uma classe que representa a ausência de valor.

A primeira questão que precisamos resolver ao implementar esse padrão é pensar em um bom nome para representar nossa classe, como por exemplo:

class Endereco { ... }
class Autor { ... }
class Permissao { ... }
// se o endereço é null, o endereço é desconhecido:
class EnderecoDesconhecido { ... }
// se o autor é null, o autor é anônimo:
class AutorAnonimo { ... }
// se a permissão é null, ou ela está pendente, ou foi negada:
class PermissaoPendente { ... }
class PermissaoNegada { ... }

Depois disso, tudo o que precisamos fazer é implementar os mesmos métodos que a classe original implementa, porém retornando valores apropriados para um valor nulo, como por exemplo:

// Endereço
class Endereco
{
    public function getDescricao()
    {
        return implode(', ', [
            $this->logradouro,
            $this->numero,
            $this->bairro,
            $this->cidade,
            $this->uf,
        ]);
    }
}
class EnderecoDesconhecido extends Endereco
{
    public function getDescricao()
    {
        return 'Endereço desconhecido.';
    }
}
// Autor
class Autor
{
    public function getNomeCompleto()
    {
        return $this->nome . ' ' . $this->sobrenome;
    }
}
class AutorAnonimo extends Autor
{
    public function getNomeCompleto()
    {
        return 'Anônimo';
    }
}
// Permissão
class Permissao
{
    public function verificar($acao)
    {
        return !empty($this->permissoes[$acao]);
    }
}
class PermissaoPendente extends Permissao
{
    public function verificar($acao)
    {
        return false;
    }
}

Como nunca teremos um valor null, mas sim ou o objeto original ou o objeto que representa a ausência dele, nunca precisamos checar por null: basta invocar os métodos que o sistema funcionará normalmente.

Exemplo de Aplicação

No exemplo abaixo, um usuário pode ou não pagar por uma assinatura em um site. Caso ele seja um assinante, a assinatura é associada com o seu perfil, caso contrário, ela simplesmente tem o valor null:

class Usuario
{
    public function cobrar()
    {
        // aqui
        if (!empty($this->assinatura)) {
            $this->assinatura->cobrar($this->getCartaoCredito());
        }
    }

    public function isPremium()
    {
        // aqui
        return !empty($this->assinatura) && $this->assinatura->isPremium();
    }

    public function getValorCobrado()
    {
        // aqui
        if (!empty($this->assinatura)) {
            return $this->assinatura->getValor();
        }
        return 0;
    }
}
class Assinatura
{
    public function isPremium()
    {
        return $this->isPremium;
    }

    public function getValor()
    {
        return $this->valor;
    }

    public function cobrar(CartaoCredito $cartao)
    {
        $cartao->cobrar($this->getValor());
    }
}

Vamos refatorar o código acima introduzindo uma classe chamada AssinaturaGratis para nos referirmos à ausência de uma assinatura vinculada à conta do usuário:

class AssinaturaGratuita extends Assinatura
{
    public function isPremium()
    {
        return false;
    }

    public function getValor()
    {
        return 0;
    }

    public function cobrar(CartaoCredito $cartao)
    {
    }
}
// Utilização
if (null === $assinatura) {
    $assinatura = new AssinaturaGratuita;
}

(Alternativamente, poderíamos criar uma interface que as duas classes implementam, ao invés da classe AssinaturaGratuita estender de Assinatura.)

Agora que temos nosso objeto nulo, podemos remover todos os ifs das assinaturas e invocar os métodos dela normalmente, sabendo que ela nunca será null:

class Usuario
{
    public function cobrar()
    {
        $this->assinatura->cobrar($this->getCartaoCredito()); // sem testar por null!
    }

    public function isPremium()
    {
        $this->assinatura->isPremium(); // sem testar por null!
    }

    public function getValorCobrado()
    {
        return $this->assinatura->getValor(); // sem testar por null!
    }
}

Todos os if ($assinatura) foram removidos, e agora chamamos $this->assinatura diretamente sem medo.

Formas de Implementar a Classe Nula

Para implementar o padrão, é importante considerar como isso será feito: se você prefere criar uma interface e fazer ambas as classes implementarem-na ou se fará a classe nula estender a original, ou vice-versa:

// Usando uma interface:
interface AssinaturaInterface
{
    public function isPremium();
    public function getValor();
    public function cobrar(CartaoCredito $cartao);
}

class Assinatura implements AssinaturaInterface { ... }
class AssinaturaGratuita implements AssinaturaInterface { ... }

// Classe nula estendendo a original:
class AssinaturaGratuita extends Assinatura { ... }

// Classe original estendendo a nula:
class Assinatura extends AssinaturaGratuita { ... }

Apesar de ter utilizado a segunda forma nos exemplos para mantê-los simples, particularmente prefiro utilizar interfaces, pois assim evito problemas caso os métodos da classe-mãe mudem no decorrer da vida do programa.

Conclusão

Utilizar objetos nulos é uma ótima forma de reduzir a complexidade do nosso código e tornar o nosso trabalho mais feliz.

Como na vida nem tudo são flores, eventualmente precisamos apelar ao bom e velho null, pois nem sempre será uma boa ideia introduzir um objeto ou lançar uma exceção na ausência de um valor.

Referências