Back-End

10 out, 2016

Evitando objetos quase-imutáveis no PHP

Publicidade

tl;dr: Imutabilidade no PHP é mais prático quando as propriedades do objeto são escalares ou nulas. Usar streams, objetos ou matrizes como propriedades torna muito difícil, por vezes impossível, preservar a imutabilidade.

Uma das táticas no Domain Driven Design é usar  Value Objects. Um value object não tem identificador ligado a ele; somente a combinação dos valores das suas propriedades é que dá qualquer identificação. Se você alterar qualquer uma das propriedades de qualquer forma, a modificação deve retornar uma instância inteiramente nova do objeto de valor.

Esse tipo de comportamento significa que o value object é “imutável”. Ou seja, a instância específica não está autorizada a ser alterada, embora você possa obter de volta uma nova instância com valores modificados. O código para um objeto imutável é algo como isto:

<?php
class ImmutableFoo
{
    protected $bar;

    public function __construct($bar)
    {
        $this->bar = $bar;
    }

    public function getBar()
    {
        return $this->bar;
    }

    public function withBar($newBar)
    {
        $clone = clone $this;
        $clone->bar = $newBar;
        return $clone;
    }
}
?>

(Note como $bar só é acessível através de um método, não como uma propriedade pública.)

Quando você cria uma instância ImmutableFoo, você não pode alterar o valor de $bar após a instanciação. Em vez disso, você só pode obter de volta uma nova instância com o novo valor de $bar chamando withBar():

<?php
$foo = new ImmutableFoo('a');
$newFoo = $foo->withBar('b');

echo $foo->getBar(); // 'a'
echo $newFoo->getBar(); // 'b'
var_dump($foo === $newFoo); // (bool) false
?>

Com essa abordagem, você está garantindo que nesse lugar no código não é possível alterar o objeto $foo a distância de qualquer outro lugar no código. Qualquer coisa que pegar essa instância $foo sabe que suas propriedades serão sempre as mesmas, não importa o que aconteça.

A abordagem de imutabilidade pode ser poderosa em Domain Driven Design e em outros lugares. Ela funciona muito facilmente em PHP com valores escalares e nulos. Isso porque o PHP os retorna por cópia, e não por referência.

No entanto, a aplicação de imutabilidade em PHP é difícil quando as propriedades do objeto imutável são não-escalares (ou seja, quando eles são streams, objetos ou arrays). Com não-escalares, o objeto pode parecer imutável no início, mas a mutabilidade se revela mais tarde. Esses objetos serão “quase”,  e não realmente imutáveis.

Streams como propriedades imutáveis

Se um stream ou recurso semelhante tiver sido aberto em um modo gravável (ou adicionável), e for utilizado como uma propriedade imutável, deve ser óbvio que a imutabilidade do objeto não é preservada. Por exemplo:

<?php
file_put_contents('/tmp/bar.txt', 'baz');

$foo = new ImmutableFoo(fopen('/tmp/bar.txt', 'w+'));
$bar = $foo->getBar();
fpassthru($bar); // 'baz'

rewind($bar);
fwrite($bar, 'dib');
rewind($bar);

fpassthru($foo->getBar()); // 'dib'
?>

Como você pode ver, o valor da propriedade mudou, o que significa que a imutabilidade foi comprometida.

Uma maneira de contornar isso pode ser se certificar de que os próprios objetos imutáveis que verificam que os recursos de stream são sempre e somente em modo de leitura. No entanto, mesmo isso pode não ser uma solução certa, porque o ponteiro de recursos pode ser deslocado por operações de leitura em diferentes partes do código da aplicação. Por sua vez, isso significa que a leitura do stream pode produzir resultados diferentes em tempos diferentes, fazendo com que o valor exibido seja mutável.

Como tal, parece que apenas streams “read-only” podem ser usados como propriedades imutáveis e, apenas se o objeto imutável restaura o stream, os ponteiros, e todos os seus metadados ao seu estado inicial cada vez que o stream é acessado.

Objetos como propriedades imutáveis

Como o PHP retorna objetos como referências, em vez de como cópias, usar um objeto como um valor de propriedade compromete a imutabilidade do objeto pai. Por exemplo:

$foo = new ImmutableFoo((object) ['baz' => 'dib']);
$bar = $foo->getBar();
echo $bar->baz; // 'dib'

$bar->baz = 'zim';
echo $foo->getBar()->baz; // 'zim'

Como você pode ver, o valor de $bar mudou na instância $foo. Qualquer outro código usando $foo vai ver essas mudanças também. Isso significa que a imutabilidade não foi preservada.

Uma maneira de contornar isso é ter certeza de que todos os objetos usados como propriedades imutáveis são eles próprios imutáveis.

Outra maneira de contornar isso é ter certeza de que os métodos getter fazem um clone de quaisquer propriedades do objeto que eles retornam. No entanto, ele terá que ser um clone recursivamente profundo, cobrindo todas as propriedades do objeto clonado (e todas as suas propriedades etc.). Isso é para ter certeza de que todas as propriedades do objeto em toda a linha abaixo também são clonadas; caso contrário, a imutabilidade é novamente comprometida em algum ponto.

Arrays como propriedades imutáveis

Ao contrário dos objetos, o PHP retorna arrays como cópias por padrão. No entanto, se uma propriedade do objeto imutável é um array, objetos mutáveis no array comprometem a imutabilidade do objeto pai. Por exemplo:

$foo = new ImmutableFoo([
    0 => (object) ['baz' => 'dib'],
]);

$bar = $foo->getBar();
echo $bar[0]->baz;

$bar[0]->baz = 'zim';
echo $foo->getBar()[0]->baz; // 'zim'

Como o array contém um objeto e o PHP retorna objetos por referência, o conteúdo do array agora mudou. Isso significa que $foo efetivamente mudou também. Mais uma vez, a imutabilidade não foi preservada.

Da mesma forma, se o array contém uma referência a um recurso de stream, vemos os problemas descritos sobre streams acima.

A única maneira de contornar isso é o objeto imutável recursivamente fazer um scan por todas as propriedades do array para ter certeza de que eles contêm apenas valores imutáveis. Isso não é, provavelmente, muito prático na maioria das situações, o que significa que os arrays provavelmente não são adequados como valores imutáveis.

Ajustes em propriedades públicas indefinidas

Finalmente, o PHP permite que você defina os valores das propriedades não definidas, como se fossem públicas. Isso significa que é possível adicionar propriedades mutáveis a um objeto imutável:

$foo = new ImmutableFoo('bar');

// there is no $zim property, so PHP
// creates it as if it were public

$foo->zim = 'gir';
echo $foo->zim; // 'gir'

$foo->zim = 'irk';
echo $foo->zim; // 'irk'

A imutabilidade do objeto é uma vez mais comprometida. A única maneira de contornar isso é implementar __set() para impedir a definição de propriedades indefinidas.

Além disso, pode ser sábio implementar __unset() para avisar que as propriedades não podem ser desativadas.

Conclusão

Se você quer construir um objeto verdadeiramente imutável em PHP, parece que a melhor abordagem é a seguinte:

  • O padrão é usar apenas escalares e nulos como propriedades.
  • Evite streams como propriedades; se uma propriedade deve ser um stream, tenha certeza de que ela é somente de leitura, e que seu estado é restaurado cada vez que é usado.
  • Evite objetos como propriedades; se uma propriedade deve ser um objeto, tenha certeza de que o objeto é em si imutável.
  • Especialmente evite arrays como propriedades; use somente com extrema cautela e cuidado.
  • Implemente __set() para não permitir configuração de propriedades indefinidas.
  • Possivelmente implemente __unset() para avisar que o objeto é imutável.

No geral, parece que a imutabilidade é mais fácil com apenas escalares e nulos. Se estiver diante de qualquer outra coisa, há muito mais possibilidade de ter um erro.

Lembre-se, no entanto, de que não há nada de errado com objetos total ou parcialmente mutáveis, contanto que eles sejam anunciados como tal. O que você quer evitar são objetos quase-imutáveis: os que anunciam, mas não entregam, verdadeira imutabilidade.

(Para uma leitura adicional, leia At What Point Do Immutable Classes Become A Burden?)

Atualização:

***

Paul M. Jones faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://paul-m-jones.com/archives/6400.