paint-brush
Aprenda a vivir con objetos inmutables y confiables en Javapor@artemsutulov
621 lecturas
621 lecturas

Aprenda a vivir con objetos inmutables y confiables en Java

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

Demasiado Largo; Para Leer

Al escribir proyectos complejos, es esencial desarrollar una buena cultura de código. El uso de objetos inmutables y consistentes es uno de los más importantes. Puede descuidar esto y escribir objetos complejos como estándar, pero será una fuente de errores cuando el proyecto crezca lo suficiente. En este artículo, mostraré cómo hacer que los objetos sean inmutables y hacerlos de manera elocuente y eficiente. La forma predeterminada no tendrá setters con un objeto sin setters. La única forma de hacerlo es crear una nueva instancia y colocar nuevos valores en la propia clase responsable.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Aprenda a vivir con objetos inmutables y confiables en Java
Artem Sutulov HackerNoon profile picture

Al escribir proyectos complejos, es esencial desarrollar una buena cultura de código. El uso de objetos inmutables y consistentes es uno de los más importantes.


Puede ignorar esto y escribir objetos complejos como estándar, pero será una fuente importante de errores cuando el proyecto crezca lo suficiente.


En miartículo anterior, mostré cómo podemos mejorar la consistencia y confiabilidad de los objetos estándar. En pocas palabras:


  • Agregar validaciones al establecer valores

  • Use java.util.Optional para cada campo anulable

  • Coloque las mutaciones complejas en un lugar adecuado, para la clase responsable misma.


Pero esas acciones no son suficientes para hacer objetos completamente confiables. En este artículo, mostraré cómo hacer que los objetos sean inmutables y hacerlos de manera elocuente y eficiente.

El problema

Si creamos un objeto serializable simple con un constructor/getters/setters predeterminado, está bien hacerlo de la manera estándar. Pero supongamos que escribimos algo más complejo. Y lo más probable es que se use en innumerables lugares.


Por ejemplo, se usa en HashMaps y tal vez en un entorno de subprocesos múltiples. Entonces, ya no parece una buena idea escribirlo de la manera predeterminada. Romper HashMap no es gran cosa, y un estado inconsistente entre subprocesos no hará esperar mucho.


Las primeras cosas que vienen a la mente cuando se piensa en hacer objetos inmutables son:


  1. No haga métodos setter
  2. Hacer que todos los campos sean definitivos
  3. No comparta instancias con objetos mutables
  4. No permita que las subclases anulen los métodos (lo omitiré en este artículo)


Pero, ¿cómo vivir con ese tipo de objeto? Cuando necesitamos cambiarlo, necesitamos hacer una copia; ¿Cómo podemos hacerlo bien sin copiar y pegar código y lógica cada vez?

Algunas palabras sobre nuestras clases de ejemplo

Digamos que tenemos Cuentas. Cada cuenta tiene una id , status y email . Las cuentas se pueden verificar por correo electrónico. Cuando el estado es CREATED , no esperamos que se complete el correo electrónico. Pero cuando está VERIFIED o INACTIVE , se debe llenar el correo electrónico.



estados de cuenta


 public enum AccountStatus { CREATED, VERIFIED, INACTIVE }


La implementación 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 }


Imaginemos que hemos creado una cuenta. Luego, en algún lugar de la lógica comercial, necesitamos cambiar un correo electrónico.


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


¿Cómo podemos hacer eso? La forma predeterminada no funcionará, no podemos tener setters con una clase inmutable.


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


La única forma de hacerlo es crear una nueva instancia y colocar los valores anteriores del constructor:


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


Pero no es la mejor manera de cambiar el valor de un campo, produce mucho copiar/pegar y la clase Account no es responsable de su consistencia.

Solución

La solución sugerida es proporcionar métodos de mutación de la clase Cuenta e implementar la lógica de copia dentro de la clase responsable. Además, es esencial agregar las validaciones requeridas y el uso de Opcional para el campo de correo electrónico, por lo que no tendremos problemas de coherencia o NPE.


Para construir un objeto, uso el patrón 'Builder'. Es bastante famoso y hay muchos complementos para que su IDE lo genere automáticamente.


 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 puede ver, hay una copy de método privado en nuestra clase que devuelve Builder con una copia exacta. Eso elimina el copiar y pegar de todos los campos, sin embargo, es crucial que este método no sea accesible desde fuera de Account.java porque, con ese Builder afuera, perdemos el control sobre el estado y la consistencia.

Cambiar de cuenta de una forma nueva

Ahora, vamos a crear una cuenta:


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


Cuando necesitamos cambiar un correo electrónico, no necesitamos ser responsables de crear una copia, simplemente llamamos a un método desde Account en sí:


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


Demostración de eso en una prueba unitaria:


 @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 una cuenta, no creamos una copia con estado VERIFIED y un nuevo correo electrónico. Simplemente llamamos al método verify , que no solo creará una copia para nosotros, sino que también verificará la validez de un correo electrónico.


 @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); }


Conclusión

Vivir con objetos inmutables, consistentes y confiables es difícil, pero en la forma adecuada, puede volverse mucho más fácil.


Al implementar uno, no te olvides de :

  1. Hacer que todos los campos sean definitivos
  2. No proporcionar setters
  3. No compartir enlaces a objetos mutables
  4. No permita que las subclases anulen los métodos.
  5. Proporcione métodos de mutación de su clase
  6. Implementar el privado copy el método dentro de la clase responsable que devuelve un Builder y utilícelo para crear nuevas instancias dentro de su clase.
  7. Mantenga la consistencia de los campos mediante el uso de validaciones en los valores
  8. Use Optional con campos anulables


Puede encontrar el ejemplo completamente funcional con más pruebas unitarias en GitHub .


Foto principal de Adam Nieścioruk en Unsplash