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.
Curso Amazon Web Services (AWS) - RDS - Fundamentos
Conhecer o cursoImutabilidade
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.
Curso Laravel - Eloquent ORM Avançado
Conhecer o cursoObjetos 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()
esub()
, 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!