Tutoriel pour utiliser efficacement les enums Java avec le patron de conception Visitor

Gregory Elhaimer

Les enums peuvent être vues comme un regroupement de constantes fortement typées. Elles trouvent leur utilité dans de nombreux usages : attribuer une sémantique forte à des valeurs, borner et valider les valeurs possibles d'une donnée, améliorer la lisibilité du code, etc. Leur utilisation peut cependant devenir acrobatique lorsqu'il s'agit de baser des décisions sur leurs valeurs. À mesure que l'enum évolue, chaque endroit où celle-ci a été utilisée en tant que condition doit être revérifié. Si la valeur d'une enum est activement exploitée dans la logique métier, maintenir la base de code dans son ensemble peut devenir un cauchemar : un oubli de vérification peut entraîner une corruption du système dans son ensemble.

N'existe-t-il pas alors un moyen de réduire l'impact des évolutions d'une enum sur le code métier ?

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum Commentez .

- Titre : Tutoriel pour utiliser efficacement les enums Java avec le patron de conception Visitor

- Auteur : Gregory Elhaimer

- Parution : 21 avril 2021

- Licence : Copyright ® 2021 Gregory Elhaimer. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. L'approche switch-case

- II. Le pattern Visitor à la rescousse

- III. Ajoutons une nouvelle asset class

- IV. En bref

- V. Remerciements

I - L'approche switch-case

Considérons l'enum AssetClass représentant les différents groupes de matières premières échangeables :

1. public enum AssetClass {

2. METAL,

3. ENERGY,

4. AGRICULTURAL,

5. }

Cette enum pourra être utilisée afin de décider des stratégies, mapper ou tout autre comportement dépendant de la valeur de AssetClass. Imaginons utiliser cette enum pour définir une stratégie de trading automatique (terme couramment utilisé dans le domaine de la finance) à utiliser. Cela ressemblera à quelque chose comme :

1. public AutomatedTradingStrategy getAutomatedTradingStrategy(AssetClass assetClass) {

2. switch (assetClass) {

3. case METAL:return new HedgingStrategy();

4. case ENERGY: return new SwingTradingStrategy();

5. case AGRICULTURAL:

6. default: return DayTradingStrategy();

7. }

8. }

Le switch-case est probablement la manière la plus simple et la plus directe de faire. Cela a cependant plusieurs défauts.

La méthode getAutomatedTradingStrategy() retourne un comportement en fonction de la valeur de AssetClass utilisée. Définir un comportement par défaut devient alors obligatoire, même si, dans cet exemple, l'ensemble des valeurs de l'enum est traité. Pour cela, nous pouvons soit retourner une implémentation de AutomatedTradingStrategy par défaut, soit retourner null ou alors émettre une exception.

L'utilisation de ce comportement par défaut rendra silencieux l'ajout d'une nouvelle valeur à l'enum. Il faudra alors penser à revérifier chaque endroit où AssetClass est impliquée dans des règles métiers. Et rien ne nous protège d'un oubli.

Le problème suivant est probablement le moins évident. L'utilisation du switch-case crée ici un couplage fort entre la logique métier et les valeurs de l'enum, brisant ainsi le principe ouvert/fermé : le code doit être ouvert à l'extension, mais fermé à la modification. Ici, modifier l'enum implique de modifier chaque bloc de code qui reposait sur ses valeurs. Pourtant, nous n'avons aucun intérêt à savoir si une asset est représentée par une enum, un objet ou autre. Seule sa sémantique compte.

Par exemple, les métaux pourraient être divisés en deux sous-catégories : les métaux précieux et les métaux de base. Tout code reposant sur AssetClass.METAL devra alors être retravaillé afin de prendre en compte ces deux nouvelles valeurs. Le refactoring de l'existant n'apportera aucune nouvelle valeur métier, mais exposera un code déjà opérationnel à des risques de régressions.

II - Le pattern Visitor à la rescousse

Comment pouvons-nous alors briser ce couplage tout en offrant la possibilité de contextualiser la logique métier aux valeurs de l'enum ? La réponse est dans le titre : utilisons le pattern Visitor.

Créons dans un premier temps l'interface qui servira de contrat entre notre enum et le code souhaitant interagir avec elle.

1. public interface AssetClassVisitor<T> {

2. T visitMetal();

3. T visitEnergy();

4. T visitAgricultural();

5. }

L'interface est générique afin que celle-ci puisse permettre des implémentations dont l'objectif diffère selon son contexte d'utilisation.

Il est maintenant nécessaire de modifier l'enum afin que celle-ci accepte toute demande respectant le contrat porté par AssetClassVisitor :

1. public enum AssetClass {

2. METAL {

3. @Override

4. public <E> E accept(AssetClassVisitor<E> visitor) {

5. return visitor.visitMetal();

6. }

7. },

8. ENERGY {

9. @Override

10. public <E> E accept(AssetClassVisitor<E> visitor) {

11. return visitor.visitEnergy();

12. }

13. },

14. AGRICULTURAL {

15. @Override

16. public <E> E accept(AssetClassVisitor<E> visitor) {

17. return visitor.visitAgricultural();

18. }

19. };

21. public abstract <E> E accept(AssetClassVisitor<E> visitor);

22. }

Il ne reste alors plus qu'à l'utiliser. Remplaçons notre switch-case par une implémentation du visitor :

1. public AutomatedTradingStrategy getAutomatedTradingStrategy(AssetClass assetClass) {

2. return assetClass.accept(new AssetClassVisitor<AutomatedTradingStrategy>() {

3. @Override

4. public AutomatedTradingStrategy visitMetal() {

5. return new HedgingStrategy();

6. }

8. @Override

9. public AutomatedTradingStrategy visitEnergy() {

10. return new SwingTradingStrategy();

11. }

13. @Override

14. public AutomatedTradingStrategy visitAgricultural() {

15. return new DayTradingStrategy();

16. }

17. });

18. }

Comme on peut le constater, chaque valeur de AssetClass porte la responsabilité d'appeler la méthode du visitor appropriée. Il est désormais inutile de connaître les valeurs ou l'implémentation d'AssetClass. AssetClass.AGRICULTURAL pourrait alors être renommé en AssetClass.AGRI sans avoir à modifier quoi que ce soit au niveau de la logique métier. Il est par ailleurs devenu inutile de gérer des comportements par défaut. Les possibilités sont désormais restreintes à celles fournies par l'interface.

III - Ajoutons une nouvelle asset class

Notre entreprise évolue et nous devons désormais étendre nos activités aux bétails et viandes. Il suffit alors d'ajouter la valeur à notre enum et de mettre à jour notre contrat d'interface.

1. public enum AssetClass {

2. METAL {

3. @Override

4. public <E> E accept(AssetClassVisitor<E> visitor) {

5. return visitor.visitMetal();

6. }

7. },

8. ENERGY {

9. @Override

10. public <E> E accept(AssetClassVisitor<E> visitor) {

11. return visitor.visitEnergy();

12. }

13. },

14. AGRICULTURAL {

15. @Override

16. public <E> E accept(AssetClassVisitor<E> visitor) {

17. return visitor.visitAgricultural();

18. }

19. },

20. // La nouvelle valeur

21. LIVESTOCK_AND_MEAT {

22. @Override

23. public <E> E accept(AssetClassVisitor<E> visitor) {

24. return visitor.visitLiveStockAndMeat();

25. }

26. };

28. public abstract <E> E accept(AssetClassVisitor<E> visitor);

29. }

1. public interface AssetClassVisitor<T> {

2. T visitMetal();

3. T visitEnergy();

4. T visitAgricultural();

5. // La nouvelle méthode

6. T visitLiveStockAndMeat();

7. }

Après cela, le code va s'allumer comme un sapin de Noël : plus rien ne compile. Et le compilateur devrait être remercié d'avoir fait un si bon travail ! Toutes ces erreurs mises en évidence de toute part nous montrent que certaines parties du code ne sont pas en mesure de répondre à cette nouvelle valeur. Corrigeons donc cela en utilisant une exception : le commerce des bétails et viandes n'est pas automatisable.

1. public AutomatedTradingStrategy getAutomatedTradingStrategy(AssetClass assetClass) {

2. return assetClass.accept(new AssetClassVisitor<AutomatedTradingStrategy>() {

3. @Override

4. public AutomatedTradingStrategy visitMetal() {

5. return new HedgingStrategy();

6. }

8. @Override

9. public AutomatedTradingStrategy visitEnergy() {

10. return new SwingTradingStrategy();

11. }

13. @Override

14. public AutomatedTradingStrategy visitAgricultural() {

15. return new DayTradingStrategy();

16. }

18. @Override

19. public AutomatedTradingStrategy visitLiveStockAndMeat() {

20. throw new AutomatedTradingNotSupported("Automated trading for Livestock and meat is not allowed.")

21. }

22. });

23. }

IV - En bref

Lors d'une de mes missions, l'équipe a été confrontée à un nombre conséquent d'enums et de logiques métier basées sur leurs valeurs. Le pattern Visitor était notre bouclier contre les cas à la marge au point d'en devenir notre standard dans la gestion des enums.

Utiliser ce pattern n'est pas nécessaire si les enums sont purement descriptives. Cependant, sortir l'artillerie lourde vaut définitivement le coût de développement supplémentaire. Briser le couplage entre la valeur d'une enum et la logique métier offre une souplesse d'évolution supplémentaire tandis que le compilateur réduit la boucle de feedback en mettant en lumière les oublis potentiels.

V - Remerciements

Cet article a été publié avec l'aimable autorisation de Gregory Elhaimer. L'article original peut être vu sur le blog de la société Ippon.

Nous remercions également Winjerome pour la mise au gabarit et escartefigue pour la relecture orthographique de cet article.