paint-brush
Aprenda a viver com objetos imutáveis e confiáveis em Javapor@artemsutulov
624 leituras
624 leituras

Aprenda a viver com objetos imutáveis e confiáveis em Java

por Artem Sutulov1m2022/05/28
Read on Terminal Reader
Read this story w/o Javascript

Muito longo; Para ler

Ao escrever projetos complexos, é essencial desenvolver uma boa cultura de código. O uso de objetos imutáveis e consistentes é um dos mais importantes. Você pode negligenciar isso e escrever objetos complexos como padrão, mas será uma fonte de bugs quando o projeto crescer o suficiente. Neste artigo, mostrarei como tornar objetos imutáveis e torná-los eloquentes e eficientes. A forma padrão não terá setters com um objeto sem setters. A única maneira de fazer isso é criar uma nova instância e colocar novos valores na própria classe responsável.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Aprenda a viver com objetos imutáveis e confiáveis em Java
Artem Sutulov HackerNoon profile picture

Ao escrever projetos complexos, é essencial desenvolver uma boa cultura de código. O uso de objetos imutáveis e consistentes é um dos mais importantes.


Você pode negligenciar isso e escrever objetos complexos como padrão, mas será uma fonte significativa de bugs quando o projeto crescer o suficiente.


Em meuartigo anterior, mostrei como podemos melhorar a consistência e a confiabilidade de objetos padrão. Em poucas palavras:


  • Adicionar validações ao definir valores

  • Use java.util.Optional para cada campo anulável

  • Coloque as mutações complexas em um local apropriado - para a própria classe responsável.


Mas essas ações não são suficientes para criar objetos totalmente confiáveis. Neste artigo, mostrarei como tornar objetos imutáveis de maneira eloquente e eficiente.

O problema

Se fizermos um objeto serializável simples com construtor/getters/setters padrão, não há problema em torná-lo da maneira padrão. Mas vamos supor que escrevemos algo mais complexo. E muito provavelmente, é usado em inúmeros lugares.


Por exemplo, é usado em HashMaps e talvez em um ambiente multi-threading. Portanto, não parece mais uma boa ideia - escrevê-lo da maneira padrão. Quebrar o HashMap não é grande coisa, e um estado inconsistente entre as threads não vai demorar muito.


As primeiras coisas que vêm à mente quando se pensa em fazer objetos imutáveis são:


  1. Não faça métodos setter
  2. Tornar todos os campos finais
  3. Não compartilhe instâncias com objetos mutáveis
  4. Não permita que subclasses substituam métodos (vou omiti-lo neste artigo)


Mas como conviver com esse tipo de objeto? Quando precisamos alterá-lo, precisamos fazer uma cópia; como podemos fazer isso bem sem copiar e colar código e lógica toda vez?

Algumas palavras sobre nossas classes de exemplo

Digamos que temos contas. Cada conta tem um id , status e e- email . As contas podem ser verificadas por e-mail. Quando o status é CREATED , não esperamos que o e-mail seja preenchido. Mas quando for VERIFIED ou INACTIVE , o e-mail deve ser preenchido.



Status da conta


 public enum AccountStatus { CREATED, VERIFIED, INACTIVE }


A implementação canon Account.java :


 public class Account { private final String id; private final AccountStatus status; private final String email; public Account(String id, AccountStatus status, String email) { this.id = id; this.status = status; this.email = email; } // equals / hashCode / getters }


Vamos imaginar que criamos uma conta. Então, em algum lugar da lógica de negócios, precisamos alterar um e-mail.


 var account = new Account("some-id", CREATED, null);


Como podemos fazer isso? A maneira padrão não funcionará, não podemos ter setters com uma classe imutável.


 account.setEmail("[email protected]");// we can not do that, we have no setters


A única maneira de fazer isso é criar uma nova instância e colocar os valores anteriores do construtor:


 var withEmail = new Account(account.getId(), CREATED, "[email protected]");


Mas não é a melhor maneira de alterar o valor de um campo, isso produz muito copiar/colar, e a classe Account não é responsável por sua consistência.

Solução

A solução sugerida é fornecer métodos de mutação da classe Account e implementar a lógica de cópia dentro da classe responsável. Além disso, é essencial adicionar as validações necessárias e o uso de Opcional para o campo de email, para que não tenhamos NPE ou problemas de consistência.


Para construir um objeto, eu uso o padrão 'Builder'. É bem famoso, e tem vários plugins para sua IDE gerar automaticamente.


 public class Account { private final String id; private final AccountStatus status; private final Optional<String> email; public Account(Builder builder) { this.id = notEmpty(builder.id); this.status = notNull(builder.status); this.email = checkEmail(builder.email); } public Account verify(String email) { return copy() .status(VERIFIED) .email(of(email)) .build(); } public Account changeEmail(String email) { return copy() .email(of(email)) .build(); } public Account deactivate() { return copy() .status(INACTIVE) .build(); } private Optional<String> checkEmail(Optional<String> email) { isTrue( notNull(email).map(StringUtils::isNotBlank).orElse(false) || this.status.equals(CREATED), "Email must be filled when status %s", this.status ); return email; } public static final class Builder { private String id; private AccountStatus status; private Optional<String> email = empty(); private Builder() { } public static Builder account() { return new Builder(); } public Builder id(String id) { this.id = id; return this; } public Builder status(AccountStatus status) { this.status = status; return this; } public Builder email(Optional<String> email) { this.email = email; return this; } public Account build() { return new Account(this); } } // equals / hashCode / getters }


Como você pode ver, existe uma copy de método privado em nossa classe que retorna o Builder com uma cópia exata. Isso remove o copy-pasting de todos os campos, no entanto, é crucial que esse método não seja acessível de fora do Account.java porque, com esse Builder fora, perdemos o controle sobre o estado e a consistência.

Alterando contas de uma nova maneira

Agora, vamos criar uma conta:


 var account = account() .id("example-id") .status(CREATED) .email((empty()) .build();


Quando precisamos alterar um e-mail, não precisamos nos responsabilizar por criar uma cópia, basta chamar um método da própria Account :


 var withNewEmail = account.changeEmail("[email protected]");


Demonstração disso em um teste de unidade:


 @Test void should_successfully_change_email() { // given var account = account() .id("example-id") .status(VERIFIED) .email(of("[email protected]")) .build(); var newEmail = "[email protected]"; // when var withNewEmail = account.changeEmail(newEmail); // then assertThat(withNewEmail.getId()).isEqualTo(account.getId()); assertThat(withNewEmail.getStatus()).isEqualTo(account.getStatus()); assertThat(withNewEmail.getEmail()).isEqualTo(of(newEmail)); }


Para verificar uma conta, não criamos uma cópia com status VERIFIED e um novo e-mail. Simplesmente chamamos o método verify , que não apenas criará uma cópia para nós, mas também verificará a validade de um e-mail.


 @Test void should_successfully_verify_account() { // given var created = account() .id("example-id") .status(CREATED) .build(); var email = "[email protected]"; // when var verified = created.verify(email); // then assertThat(verified.getId()).isEqualTo(created.getId()); assertThat(verified.getStatus()).isEqualTo(VERIFIED); assertThat(verified.getEmail().get()).isEqualTo(email); }


Conclusão

Viver com objetos imutáveis, consistentes e confiáveis é difícil, mas da maneira correta pode se tornar muito mais fácil.


Ao implementar um, não se esqueça de :

  1. Tornar todos os campos finais
  2. Não fornecer configuradores
  3. Não compartilhe links para objetos mutáveis
  4. Não permitir que subclasses sobrescrevam métodos
  5. Forneça métodos de mutação de sua classe
  6. Implemente o privado copy dentro da classe responsável que retorna um Builder e use-o para criar novas instâncias dentro de sua classe.
  7. Mantenha a consistência dos campos usando validações em valores
  8. Use Optional com campos anuláveis


Você pode encontrar o exemplo totalmente funcional com mais testes de unidade no GitHub .


Foto principal de Adam Nieścioruk no Unsplash