Imutabilidade de objetos no PHP

Imutabilidade é uma característica forte nas linguagens funcionais, onde a alteração de estado não é frequente e, quando ela acontece, é controlada. Há de se observar que, o PHP, uma linguagem multi-paradigma e fracamente tipada, não implementa (ainda) nenhum mecanismo padrão para lidar com imutabilidade.

Linguagens de programação são naturalmente opinativas e é difícil fazer com que elas se comportem de uma determinada forma se não foram cultura e tecnicamente desenvolvidas para servir aquele propósito específico. Por exemplo, não adianta eu querer acessar diretamente um endereço da memória usando o PHP, a linguagem não nasceu com essa premissa, não é uma linguagem para se desenvolver sistemas embarcados, por exemplo. Consegue perceber? As linguagens possuem “culturas” e características e, quando escolhemos uma delas, temos que nos “encaixar” nesses aspectos.

Disso, infere-se que, por mais que tentemos, programar em PHP nunca será 100% thread safe, será sempre mutável. No entanto, podemos usar características da linguagem a fim de emular diferentes comportamentos e características, dentre elas, a imutabilidade (mesmo que parcial, se estritamente avaliado). Há de se destacar que, sim, é possível programar usando o paradigma funcional em PHP desde a versão 5.3 com a introdução de Lambda’s e Closure’s.

Imutabilidade

Um objeto imutável é, por definição, aquele que não pode ter o seu estado alterado depois da sua criação.

Abrindo um parênteses aqui, uma constante é a única estrutura realmente imutável no PHP. Tanto as constantes do escopo global quanto as constantes de classe garantem que o valor declarado permanecerá estático durante todo o ciclo de vida de execução do script.

class FooClass
{
    const PI = 3.14159265359;

    public function __construct()
    {
        static::PI = 3.14; // FATAL ERROR syntax error, unexpected '=' on line number 7
    }
}

$foo = new FooClass(); // Error

Mas, constantes só trabalham com valores escalares (int, string, float, boolean), nulos e arrays (um tipo composto).

Objetos mutáveis

A alternância de estados é uma das características mais fortes do paradigma imperativo, logo, essa também é uma característica evidente no paradigma orientado a objetos (que é imperativo). Há muitos benefícios, claro. Os princípios SOLID são conceitualmente incríveis, aumentam a legibilidade e a manutenibilidade dos softwares. Mas, às vezes, lidar com centenas de objetos pode nos levar à perigosas armadilhas.

Um problema que a mutabilidade pode nos trazer é o efeito colateral. Um código mutável fica a mercê de alterações não previstas do seu estado, então, num determinado ciclo da execução ele pode ter o estado A e em outro ciclo o estado B, ele fica suscetível à violações.

Vamos a um exemplo didático? Considere uma classe para trabalhar com simples cálculos monetários:

class Money
{
    /**
     * @var mixed
     */
    private $amount;

    /**
     * Money constructor.
     * @param $amount
     */
    public function __construct($amount)
    {
        $this->amount = $amount;
    }

    /**
     * @param $amount
     * @return $this
     */
    public function plus($amount)
    {
        $this->amount += $amount;

        return $this;
    }

    /**
     * @param $amount
     * @return $this
     */
    public function sub($amount)
    {
        $this->amount -= $amount;

        return $this;
    }

    /**
     * @return string
     */
    public function amount()
    {
        return $this->amount;
    }
}

Suponhamos, então, que temos uma classe Payment que utiliza a Money:

class Payment
{
    /**
     * @param $valor
     * @return array
     */
    public function process($valor)
    {
        $valorBruto = new Money($valor);

        // TODO

        $valorLiquido = $valorBruto->sub(20);

        // TODO

        return [
            'valor_bruto' => $valorBruto->amount(),
            'valor_liquido' => $valorLiquido->amount(),
        ];
    }
}

Integrando e utilizando o exemplo:

$payment = new Payment();
$result = $payment->process(100);

Avaliando a classe Payment subtende-se como senso comum que teremos armazenado na variável $result o seguinte array:

[
    'valor_bruto' => 100,
    'valor_liquido' => 80,
]

Uma vez que a entrada foi 100 e o valor líquido é o valor bruto menos 20. Certo?

Mas não é o que acontece. O resultado é:

[
    'valor_bruto' => 80,
    'valor_liquido' => 80,
]

Se você programa em PHP regularmente sabe que, quando atribuímos um objeto a uma variável, uma referência desse objeto é nos retornada. Isso quer dizer, no nosso exemplo, tanto a variável $valorBruto quanto a $valorLiquido trabalham com exatamente o mesmo endereço de objeto na memória, por isso tivemos esse efeito colateral do valor bruto ser 80 e não 100, como esperado.

Não teríamos tido tal problema se no exemplo tivéssemos explicitamente clonado o objeto $valorBruto usando o operador clone, assim:

$valorLiquido = clone $valorBruto;
$valorLiquido->sub(20);

Uma clonagem de um objeto significa a cópia de toda a estrutura interna dele, mas em outro endereço de memória. É um objeto igual em atributos / características, mas diferente.

Se alterarmos o objeto $valorBruto isso não será refletido no $valorLiquido e vice versa. São objetos distintos, moram em outro “endereço”, mesmo que iguais (no sentido de ser, ambos são do tipo Money).

Mas, que fique claro, isso não tira o fato de que esses objetos continuarão sendo suscetíveis à consecutivas mudanças de estado no ciclo de execução. É agora que entra a parte que “toca” o objetivo desse artigo.

Objetos imutáveis

Não tem como definirmos um objeto essencialmente 100% imutável no PHP, ele pode ser violado por reflexão, métodos mágicos, bindings de funções, truques com referências etc. Por exemplo, um objeto pode ser alterado pelos métodos mágicos __set(), __unset(), no uso das funções serialize() e unserialize().

No entanto, podemos chegar bem próximos disso, seguindo algumas regras:

  • Declare a classe como sendo final (a impede de ser estendida);
  • Declare as propriedades como sendo privadas;
  • Evite métodos setters, no lugar, utilize o construtor da classe para receber o valor.
  • Quando for preciso modificar o valor do objeto, retorne uma cópia (um clone) dele, nunca ele próprio;
  • Evite que esse objeto receba outro objeto e, caso seja preciso, ele também precisa ser imutável;

Vamos transformar a nossa classe Money em uma “classe imutável” usando os recursos que temos disponíveis no PHP?

final class Money
{
    /**
     * @var mixed
     */
    private $amount;

    /**
     * Money constructor.
     * @param $amount
     */
    public function __construct($amount)
    {
        if( ! is_numeric($amount)) {
            throw new \InvalidArgumentException('The amount must be numeric.');
        }

        $this->amount = $amount;
    }

    /**
     * @param $amount
     * @return Money
     */
    public function plus($amount)
    {
        return new self($this->amount + $amount);
    }

    /**
     * @param $amount
     * @return $this
     */
    public function sub($amount)
    {
        return new self($this->amount - $amount);
    }

    /**
     * @return string
     */
    public function amount()
    {
        return $this->amount;
    }
}

O que fizemos:

  • A classe agora é final;
  • Garantimos no construtor receber só o tipo de valor que queremos;
  • Nos métodos plus() e sub(), ao invés de alterarmos o objeto atual, retornamos sempre um novo objeto com o valor da operação em questão. Essa é a parte mais importante.
  • O estado de $amount agora estará protegido, ademais, é um atributo privado.

Observe os métodos plus() e sub():

/**
 * @param $amount
 * @return Money
 */
public function plus($amount)
{
    return new self($this->amount + $amount);
}

/**
 * @param $amount
 * @return $this
 */
public function sub($amount)
{
    return new self($this->amount - $amount);
}

Eles sempre vão retornar uma nova instância de Money e isso faz com os nossos objetos não tenham seus estados alterados durante o ciclo de execução.

Observe que, se queremos um objeto imutável, as alterações realizadas nele precisam criar uma nova estrutura que compartilhe características da original. Em termos gerais, tudo o que for invocado no objeto não pode alterar o estado dele, ao contrário disso, deve-se retornar o resultado dessa transformação em uma nova estrutura.

Vamos testar isso na prática?

$valor1 = new Money(20);
$valor1->sub(10);

echo $valor1->amount();

Qual será o valor da impressão?

Será 20. Estamos subtraindo 10, mas o método sub() nos retorna um novo objeto. Não estamos “tocando” no objeto $valor1. Ele permaneceu intacto.

Isso pode ser constatado se compararmos os dois objetos, veremos que são diferentes:

$valor1 = new Money(20);
$valor2 = $valor1->sub(10);

if($valor1 === $valor2) {
    echo 'São iguais';
} else {
    echo 'São diferentes.';
}

O resultado será:

São diferentes.

Alguns dos benefícios da imutabilidade:

  • A aplicação se torna um pouco mais previsível, já que o estado dos objetos não alteram durante a execução;
  • Fica mais fácil identificar onde determinado problema aconteceu, já que não há variáveis compartilhando a referência para o mesmo objeto;

Com essa alteração, você pode voltar a testar a primeira versão da classe Payment:

class Payment
{
    /**
     * @param $valor
     * @return array
     */
    public function process($valor)
    {
        $valorBruto = new Money($valor);

        // TODO

        $valorLiquido = $valorBruto->sub(20);

        // TODO

        return [
            'valor_bruto' => $valorBruto->amount(),
            'valor_liquido' => $valorLiquido->amount(),
        ];
    }
}

O resultado será:

[
    'valor_bruto' => 100,
    'valor_liquido' => 80,
]

Sem surpresas.

RFC: Immutable classes and properties

Há um RFC aberto para o PHP que discute a inclusão de classes e propriedades imutáveis no PHP. Se aprovado para alguma futura versão, teremos uma sintaxe assim:

immutable class Email
{
  public $email;

  public function __construct ($email)
  {
    // validation

    $this->email = $email;
  }
}

$email = new Email("foo@php.net");
$emailRef = &$email->email;
$emailRef = "bar@php.net" // Call will result in Fatal Error

Observe que, mesmo esse Value Object tendo o atributo $email púbico não é possível, nem fazendo um truque com referência, alterá-lo. Isso torna menos verboso a construção objetos imutáveis e sem a necessidade de terem seus membros protegidos, além de garantir uma maior segurança (tira do desenvolvedor ter que cuidar de tais detalhes de implementação).

Aproveitando a deixa, temos na standard library do PHP uma classe chamada DateTimeImmutable para trabalhar com data e hora. Um objeto dessa classe nunca têm o estado modificado, ao contrário disso, uma nova instância é sempre retornada (como fizemos com a classe Money).

Concluindo

Há situações onde o uso de objetos imutáveis são essenciais para se garantir integridade ou até mesmo um comportamento previsível. Em DDD o uso de Value Objects (que são imutáveis por essência) é bastante encorajado. Naturalmente você se deparará com esse conceito quando lidar com programação reativa com concorrências ou com qualquer outra característica da programação funcional.

Um abraço!