C# - O problema em usar objetos mutáveis

 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:


José Carlos Macoratti