Hoje veremos que usar objetos mutáveis pode nos trazer alguns problemas. |
Os tipos mutáveis são aqueles cujos membros de dados podem ser alterados após a criação da instância, enquanto que os tipos Imutáveis são aqueles cujos membros de dados não podem ser alterados após a criação da instância. Na linguagem C# strings são imutáveis.
Dessa forma devido à sua natureza inerente, os dados do tipo mutável podem ser modificados durante o tempo de execução, portanto, o tipo mutável é freqüentemente usado quando o objeto contém uma grande quantidade de dados alteráveis.
Embora o tipo mutável não seja tão thread-safe e seguro quanto o tipo imutável, ele é mais frequentemente usado com variáveis de tipo de valor, que são alocadas na pilha, melhorando o desempenho.
Mas existem cenários nos quais usar tipos mutáveis podem nos trazer problemas. Vejamos isso...
Considere a classe:
public class Produto { public int Id { get; set; } public string Nome { get; set; } public decimal Preco { get; set; } = 100m; } |
A classe
representa algum produto e quando instanciamos o produto, o preço será
inicializado automaticamente para 100.
A classe do produto será usada pela classe do cliente.
public class Cliente
{
public decimal SaldoCartao { get; set; } = 100m;
private decimal dinheiroGasto;
public void Comprar(Produto produto)
{
if (SaldoCartao < produto.Preco)
throw new InvalidOperationException("Sem dinheiro suficiente...");
dinheiroGasto += produto.Preco;
// outra logica
}
public void CheckOut()
{
SaldoCartao -= dinheiroGasto;
}
}
|
A classe cliente ao comprar um produto verifica se existe dinheiro suficiente no cartão de crédito consultando o saldo do cartão.
Se não houver saldo é lançada uma exceção caso contrário a variável dinheiroGasto é incrementada.
O método CheckOut() pode ser usado para consultar o saldo do cartão.
Criando um projeto para testar essa implementação vamos rodar o teste a seguir:
public class Tests { [Test] public void Cliente_ComDinheiroSuficiente_PodeComprarProduto() { var produto = new Produto(); var cliente = new Cliente(); cliente.Comprar(produto);
cliente.CheckOut();
Assert.AreEqual(100m, produto.Preco);
Assert.AreEqual(0m, cliente.SaldoCartao);
}
}
|
No teste instanciamos um produto e um cliente e chamamos o método Comprar() e a seguir o método CheckOut().
O resultado é que o teste vai passar:
Agora vamos supor que houve a seguinte alteração de requisito:
Alguns clientes podem comprar um produto com um desconto especial que será aplicado antes da compra ser finalizada.
Assim vamos alterar o nosso teste para refletir esse novo requisito:
public void Cliente_ComDinheiroSuficiente_PodeComprarProduto() { var produto = new Produto(); var cliente = new Cliente(); cliente.Comprar(produto);
var descontoEspecial = true;
if (descontoEspecial)
produto.Preco *= 0.5m;
cliente.CheckOut();
Assert.AreEqual(50m, produto.Preco);
Assert.AreEqual(50m, cliente.SaldoCartao);
}
|
Agora estamos aplicando um desconto antes de finalizar a compra, e, se rodarmos o teste veremos que ele vai falhar.
O assert Assert.AreEqual (50m, cliente.SaldoCartao) falhou porque cliente.SaldoCartao era 0.
Deveria ser 50, porque o preço do produto é 50.
Então, o que deu errado?
O problema ocorreu pois quando chamamos cliente.Comprar(produto), o objeto cliente incrementou a variável dinheiroGasto.
dinheiroGasto += produto.Preco;
Essa modificação
de estado é um detalhe de implementação e, portanto, esta encapsulado pelo
objeto.
Em seguida, alteramos o estado do objeto do produto aplicando um desconto
especial.
Com isso o objeto cliente não tinha conhecimento da alteração feita porque depende do seu próprio estado.
Este é precisamente o motivo pelo qual o assert sobre o cliente.SaldoCartao falhou.
E como podemos resolver este problema ?
Erros como esse acontecem com mais frequência do que você pode imaginar.
Quando alteramos o estado de um tipo de referência mutável, os efeitos colaterais nem sempre são óbvios.
A solução para o problema é a contrapartida de objetos mutáveis, ou seja, usar objetos imutáveis.
No próximo artigo veremos como usar objetos imutáveis para resolver esse problema.
"Mas Deus prova o seu amor para
conosco, em que Cristo morreu por nós, sendo nós ainda pecadores."
Romanos 5:8
Referências:
ADO .NET - Acesso Assíncrono aos dados
C# - Programação Funcional - Exemplos
C# - Coleções Imutáveis - Macoratti
C# 9.0 - Apresentando Records
C# - Os 10 Erros mais comuns dos iniciantes
C# - Otimizando o código