Um objeto é considerado imutável quando seu estado interno, após ser criado, não muda. Em alguns casos o objeto é considerado imutável mesmo que um atributo interno mude, mas o estado pareça imutável de um ponto de vista externo. Isso pode acontecer em operação que sejam lazy, por exemplo.
O uso de objetos imutáveis traz muitas vantagens e vários tipos de problemas simplesmente desaparecem.
Nesse artigo vou mostrar como criar objetos imutáveis, suas vantagens e como utiliza-los no dia a dia.
Vantagens e mais vantagens
- Objetos imutáveis são confiáveis pois eles nunca serão alterados.
- Objetos imutáveis são thread-safe, então você não terá problemas com sincronização nem aqueles bugs aleatórios difíceis de resolver quando várias threads alteram o mesmo objeto.
- Imutabilidade torna o código mais fácil de escrever, entender e dar manutenção. Também evita aqueles bugs que você precisa investigar o sistema inteiro para descobrir onde o objeto está sendo alterado.
- Objetos imutáveis podem ser cacheados, da mesma forma que o Java faz com as Strings
- Objetos imutáveis são ótimos candidatos a chave de Map e elemento de Set.
Estratégia para definir objetos imutáveis
- Defina todas as suas variáveis como private e final. Dessa forma garantimos que as variáveis não serão alteradas após o objeto ser criado
- Não utilize métodos setter, afinal suas variáveis serão final.
- Caso seu objeto receba um objeto mutável, não permita que ele seja alterado. Crie uma cópia do objeto e não permita acesso direto a ele, não retorne a referência, nada de “getObjetoMutavel()”. O estado dos objetos referenciados não deve mudar.
- Crie sua classe como final. Dessa forma garantimos que o objeto não terá subclasses e, portanto, não terá seus métodos sobrescritos.
public final class Quadrado{ private final double opacidade; private final double tamanho; public Quadrado(double tamanho, double opacidade) { this.opacidade = opacidade; this.tamanho = tamanho; } public double getOpacidade() { return opacidade; } public double getTamanho() { return tamanho; } }
Parâmetros opcionais
No exemplo anterior temos um objeto imutável com todos os seus parâmetros no construtor. Mas se alguns dos nossos parâmetros forem opcionais, a opacidade por exemplo, teremos duas opções: criar um construtor para cada situação ou passar todos os valores sempre que o objeto for construído. Imagine fazer isso com vários parâmetros, seria extremamente improdutivo e chato.
public Quadrado(double tamanho) {...} public Quadrado(double tamanho, double opacidade) {...} public Quadrado(double tamanho, double opacidade, Cor cor, ...) {...} new Quadrado(10, 0, branco, ...);
Para evitar essas duas situações podemos utilizar o builder pattern para criar o objeto.
Imutabilidade com o Builder pattern
Faremos algumas alterações na classe Quadrado para utilizar o builder. Teremos um construtor private recebendo o builder como parâmetro. Dessa forma forçamos a construção do objeto através do builder. Também incluiremos o método toBuilder. Explicarei a função desse método mais a frente.
Criaremos o nosso builder como uma inner class contendo dois construtores. Um construtor, public, contendo todos os parâmetros obrigatórios e o outro, private, que recebera o Quadrado como parâmetro. Esse construtor será utilizado para copiar o objeto e será acessado apenas pelo método toBuilder, para padronizar a cópia.
Os métodos do builder terão os mesmos nomes das variáveis sem o prefixo set, tão comum em POJOs. O retorno dos métodos será o próprio builder, dessa forma criamos uma interface fluente.
E para criar nosso Quadrado temos o método build.
package test; public class Quadrado { private final double opacidade; private final double tamanho; private Quadrado(Builder builder) { this.opacidade = builder.opacidade; this.tamanho = builder.tamanho; } public double getOpacidade() { return opacidade; } public double getTamanho() { return tamanho; } public Builder toBuilder() { return new Builder(this); } public static class Builder { private double opacidade; private double tamanho; public Builder(double tamanho) { this.tamanho = tamanho; } private Builder(Quadrado quadrado) { tamanho = quadrado.tamanho; opacidade = quadrado.opacidade; } public Builder opacidade(double opacidade) { this.opacidade = opacidade; return this; } public Builder tamanho(double tamanho) { this.tamanho = tamanho; return this; } public Quadrado build() { return new Quadrado(this); } } }
A interface fluente do builder deixa o código simples e legível.
Quadrado quadrado = new Quadrado.Builder(10) .opacidade(.2) .build();
O método toBuilder fornece uma forma simples e padronizada para copiar e editar um objeto imutável. No exemplo abaixo copiamos o objeto quadrado e alteramos a propriedade opacidade para 0.3.
Quadrado quadradoNovo = quadrado .toBuilder() .opacidade(.3) .build();
Conclusão
Imutabilidade torna o código mais fácil de escrever, entender e dar manutenção. Utilize objetos imutáveis sempre que possível, eles evitam vários tipos de bugs.
Utilizando o Builder pattern temos uma forma padronizada para criar, copiar e editar objetos imutáveis.
Este artigo foi publicado primeiro em: brgarcia.com