26 de março de 2019

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!

Head de desenvolvimento. Vasta experiência em desenvolvimento Web com foco em PHP. Graduado em Sistemas de Informação. Pós-graduando em Arquitetura de Software Distribuído pela PUC Minas. Zend Certified Engineer (ZCE) e Coffee Addicted Person (CAP).