paint-brush
Apprenez à vivre avec des objets immuables et fiables en Javapar@artemsutulov
624 lectures
624 lectures

Apprenez à vivre avec des objets immuables et fiables en Java

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

Trop long; Pour lire

Lors de l'écriture de projets complexes, il est essentiel de développer une bonne culture du code. L'utilisation d'objets immuables et cohérents est l'une des plus importantes. Vous pouvez négliger cela et écrire des objets complexes comme des objets standard, mais ce sera une source de bogues lorsque le projet se développera suffisamment. Dans cet article, je vais montrer comment rendre les objets immuables et les rendre éloquents et efficaces. La méthode par défaut n'aura pas de setters avec un objet sans setters. La seule façon de le faire est de créer une nouvelle instance et de placer de nouvelles valeurs sur la classe responsable elle-même.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Apprenez à vivre avec des objets immuables et fiables en Java
Artem Sutulov HackerNoon profile picture

Lors de l'écriture de projets complexes, il est essentiel de développer une bonne culture du code. L'utilisation d'objets immuables et cohérents est l'un des plus importants.


Vous pouvez négliger cela et écrire des objets complexes comme des objets standard, mais ce sera une source importante de bogues lorsque le projet se développera suffisamment.


Dans monarticle précédent, j'ai montré comment on peut améliorer la cohérence et la fiabilité des objets standards. En quelques mots:


  • Ajouter des validations lors de la définition des valeurs

  • Utilisez java.util.Optional pour chaque champ nullable

  • Placez les mutations complexes au bon endroit - à la classe responsable elle-même.


Mais ces actions ne suffisent pas à fabriquer des objets entièrement fiables. Dans cet article, je vais montrer comment rendre les objets immuables et les rendre éloquents et efficaces.

Le problème

Si nous créons un objet sérialisable simple avec un constructeur/getters/setters par défaut, il est normal de le faire de manière standard. Mais supposons que nous écrivions quelque chose de plus complexe. Et très probablement, il est utilisé dans d'innombrables endroits.


Par exemple, il est utilisé dans HashMaps et peut-être dans un environnement multi-threading. Donc, cela ne semble plus être une bonne idée - de l'écrire par défaut. Briser HashMap n'est pas un gros problème, et un état incohérent entre les threads ne fera pas attendre longtemps.


Les premières choses qui viennent à l'esprit lorsque l'on pense à créer des objets immuables sont :


  1. Ne faites pas de méthodes de setter
  2. Rendre tous les champs définitifs
  3. Ne partagez pas d'instances avec des objets modifiables
  4. Ne pas autoriser les sous-classes à remplacer les méthodes (je vais l'omettre dans cet article)


Mais comment vivre avec ce genre d'objet ? Lorsque nous devons le modifier, nous devons en faire une copie ; comment pouvons-nous bien le faire sans copier-coller le code et la logique à chaque fois ?

Quelques mots sur nos classes d'exemple

Disons que nous avons des comptes. Chaque compte a un id , un status et une adresse e- email . Les comptes peuvent être vérifiés par e-mail. Lorsque le statut est CREATED , nous ne nous attendons pas à ce que l'e-mail soit rempli. Mais lorsqu'il est VERIFIED ou INACTIVE , l'email doit être renseigné.



Statuts de compte


 public enum AccountStatus { CREATED, VERIFIED, INACTIVE }


L'implémentation 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 }


Imaginons que nous avons créé un compte. Ensuite, quelque part dans la logique métier, nous devons changer un e-mail.


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


Comment pouvons-nous faire cela? La méthode par défaut ne fonctionnera pas, nous ne pouvons pas avoir de setters avec une classe immuable.


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


La seule façon de le faire est de créer une nouvelle instance et de placer les valeurs précédentes du constructeur :


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


Mais ce n'est pas la meilleure façon de changer la valeur d'un champ, cela produit tellement de copier/coller, et la classe Account n'est pas responsable de sa cohérence.

La solution

La solution suggérée consiste à fournir des méthodes de mutation à partir de la classe Account et à implémenter une logique de copie à l'intérieur de la classe responsable. De plus, il est essentiel d'ajouter les validations requises et l'utilisation de Facultatif pour le champ e-mail, afin que nous n'ayons pas de problèmes de NPE ou de cohérence.


Pour construire un objet, j'utilise le pattern 'Builder'. C'est assez célèbre, et il existe de nombreux plugins pour que votre IDE le génère automatiquement.


 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 }


Comme vous pouvez le voir, il existe une copy de méthode privée dans notre classe qui renvoie Builder avec une copie exacte. Cela supprime le copier-coller de tous les champs, cependant, il est crucial que cette méthode ne soit pas accessible depuis l'extérieur de Account.java car, avec ce Builder à l'extérieur, nous perdons le contrôle de l'état et de la cohérence.

Changer de compte d'une nouvelle manière

Maintenant, créons un compte :


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


Lorsque nous devons modifier un e-mail, nous n'avons pas besoin d'être responsables de la création d'une copie, nous appelons simplement une méthode à partir du Account lui-même :


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


Démonstration de cela dans un test unitaire:


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


Pour vérifier un compte, nous ne créons pas de copie avec le statut VERIFIED et un nouvel e-mail. Nous appelons simplement la méthode verify , qui non seulement créera une copie pour nous, mais vérifiera également la validité d'un 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); }


Conclusion

Vivre avec des objets immuables, cohérents et fiables est difficile, mais de la bonne manière, cela peut devenir beaucoup plus facile.


Lors de la mise en œuvre d'un, n'oubliez pas de :

  1. Rendre tous les champs définitifs
  2. Ne pas fournir de setters
  3. Ne pas partager de liens vers des objets modifiables
  4. Ne pas autoriser les sous-classes à remplacer les méthodes
  5. Fournir des méthodes de mutation à partir de votre classe
  6. Mettre en œuvre le privé copy la méthode à l'intérieur de la classe responsable qui renvoie un Builder et utilisez-la pour créer de nouvelles instances dans votre classe.
  7. Maintenir la cohérence des champs en utilisant des validations sur les valeurs
  8. Utiliser Optional avec des champs nullables


Vous pouvez trouver l'exemple entièrement fonctionnel avec plus de tests unitaires sur GitHub .


Photo principale par Adam Nieścioruk sur Unsplash