Tutoriel sur des patterns d'implémentation avec les enums Java

Image non disponible

Cet article présente des patterns d'implémentation avec les enums Java.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum 4 commentaires Donner une note à l'article (5).

Article lu   fois.

Les deux auteurs

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Bien que les enums soient apparues en Java 5, elles restent relativement méconnues. Voyons donc quelques patterns d'implémentation. On commence par quelques points techniques (avertissement : certains sont plus des curiosités qu'autre chose) puis on aborde le pattern le plus important et le plus sous-utilisé : les enums polymorphiques.

II. Les enums Java : l'intégration du pattern « type-safe enumeration » dans le langage

Pour rappel, les enums Java sont complètement différentes des enums C#. Les enums C# sont des « value types » (comme les struct/int/Date…), représentant des alias d'int, et peuvent être combinées en bitmask. Leur philosophie est donc beaucoup plus proche des enums C++ (même si C++ 11 apporte les 'enums class' qui semblent aller plus loin qu'un simple alias d'int). Les enums Java sont l'aboutissement d'un processus différent, l'incarnation en feature d'un langage de ce qui était autrefois un pattern à implémenter par le programmeur : la 1re édition de « Effective java », qui précède l'arrivée de Java 5, détaille l'implémentation correcte en Java du pattern d'implémentation « type-safe enumerations ». Il s'agit d'un processus fréquent d'évolution d'un langage : ainsi, le pattern publish/subscribe devient une feature du langage en C#, le pattern prototype devient une feature du langage en JavaScript, etc.

À noter que l'implémentation manuelle des type-safe enums n'est pas du tout triviale : elle doit non seulement interdire l'instanciation par constructeur d'instances ne faisant pas partie de la liste des valeurs d'enum, mais aussi l'instanciation par le biais des mécanismes extralinguistiques (hors langage) : réflexivité, désérialisation et clonage.

Contrairement aux enums de type « alias d'int », les « type-safe enums » présentent les avantages suivants :

  • Il n'est pas possible d'en obtenir une instance qui ne fasse pas partie de la liste des « enum constants », contrairement à C# où il est possible de faire +1 sur une enum (sans échec à la compilation ni au runtime).
  • Elles sont polymorphiques, chaque valeur peut implémenter différemment une méthode définie par l'enum, ou par une interface implémentée par l'enum.

III. Utilisations originales des enums Java

Les deux patterns suivants ne sont pas fondamentaux, mais juste des techniques d'implémentation intéressantes dans certains cas. Ces sujets plus « légers » servent d'apéritif avant de voir une technique plus générale qui, elle, peut améliorer votre design objet global : les enums polymorphiques.

III-A. L'enum singleton

III-A-1. Le singleton naïf

NaiveSingleton.java
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
/**
 * L'implémentation naïve du singleton offre moins de garanties que l'enum,
 * surtout pour un type sérialisable.
 */
public class NaiveSingleton implements Serializable {
    private NaiveSingleton() {}
    public static final NaiveSingleton instance = new NaiveSingleton(); 
}

Il maintient bien la garantie d'instance unique en empêchant l'instanciation par constructeur depuis une autre classe, mais ne protège pas contre les instanciations par les mécanismes extralinguistiques : invocation du constructeur par réflexion, copie par sérialisation (si Serializable), clone (peu vraisemblable certes).

Les enums sont un singleton idéal, car le code généré par javac interdit ces tentatives : comme le dit bien Effective java,

Item 3: Enforce the singleton property with a private constructor or an enum type As of release 1.5, there is a third approach to implementing singletons. Simply make an enum type with one element :

Elvis.java
Sélectionnez
1.
2.
3.
4.
// Enum singleton - the preferred approach
public enum Elvis {
    INSTANCE;
}

This approach is functionally equivalent to the public field approach, except that it is more concise, provides the serialization machinery for free, and provides an ironclad guarantee against multiple instantiation, even in the face of sophisticated serialization or reflection attacks. While this approach has yet to be widely adopted, a single-element enum type is the best way to implement a singleton.

L'auteur considère cette façon de faire comme la meilleure implémentation d'un singleton, et non comme une simple ruse. S'agissant de Josh Bloch j'ai tendance à le croire, je pense que c'est seulement l'habitude qui nous fait parfois considérer cette utilisation des enums comme bizarre.

Exemple :

Factory.java
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
/**
 * Un singleton stateless. 
 * Java Puzzlers suggère d'utiliser une enum pour être sûr à moindres frais
 * que cette enum en est vraiment une.
 */
public enum Factory {
 
    instance;
 
    public Object create() {
        return new Object();
    }
}

III-B. L'enum singleton stateful

Il est déconseillé d'utiliser les enums stateful, pour les mêmes raisons que celles qui conduisent à décourager l'utilisation de singletons stateful :

  • Dépendance à l'ordre d'initialisation (indéterminisme) : l'état des singletons dépend de l'ordre dans lequel ils ont été initialisés. Or, cet ordre est difficilement maîtrisable, car déterminé au runtime par la séquence d'invocations.
  • Problèmes découlant de la création de variables globales statiques (pas d'encapsulation, fuites mémoire…).

De plus, même si les « typesafe enums » sont très différentes des enums « alias d'int », on s'attend quand même implicitement à ce qu'elles soient immutables.

Si on a besoin de singletons statefuls, il vaut mieux utiliser des beans Spring, pour lesquels le conteneur prend en charge l'initialisation, ce qui évite au moins le problème de l'ordre d'initialisation (s'il y a un cycle, Spring nous le signale au démarrage).

IV. L'enum « static class »

Ce pattern est plus une curiosité qu'autre chose. Java n'a pas de « top-level static class » au sens de C# (classes dont le compilateur s'assure que toutes les méthodes sont statiques).

L'enum sans valeurs est un pattern d'implémentation Java qui émule ces classes (qu'on pourrait facilement avoir en Java dans un « project coin 2 » via un changement de javac, mais les concepteurs de Java doivent avoir peur d'ajouter à la confusion qui entoure déjà les « static nested class » vs « inner class »).

Maths
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
public enum Maths {
 
    ;//Une enum commence toujours par la déclaration de ses instances (les "enum constants");
     //ici il n'y en a aucune mais le bloc est quand même obligatoire.
 
    public static double PI = 4*java.lang.Math.atan(1.0D); //tan(pi/4)=1 cf. le cercle trigonométrique
 
    public static double cos(double radians) {
        return 0D; //TODO
    } 
 
    //Impossible à appeler
    public double toto() {return 999D;}
}

L'émulation n'est pas complète, car on peut quand même écrire une méthode non statique, mais au runtime on est sûr de ne pas invoquer cette méthode. Dans un sens, la garantie est même supérieure au pattern habituel :

 
Sélectionnez
1.
private Math(){throw new UnsupportedOperationException();}

, qui permet de rentrer dans le constructeur (même si c'est pour crasher tout de suite), et qui est un peu plus verbeux.

V. Le vif du sujet : les Enums polymorphiques

Rentrons maintenant dans le vif du sujet avec ce pattern extrêmement puissant. Comme expliqué en introduction, contrairement à C# où ce sont des « value types » de même que les structs ou les types « primitifs », les enums Java sont polymorphiques. Il s'agit donc de « vrais objets » qu'on peut utiliser pour implémenter les design patterns faisant appel au dynamic dispatch, et ainsi éviter le code procédural (switch sur la valeur de l'enum). Un cas fréquent est l'implémentation du pattern Strategy du GOF.

V-A. Variation : implémentation complète

Dans cette variation du pattern, il n'existe pas d'implémentations non-enums. Si on a besoin d'une autre implémentation, on ajoute une constante à l'enum, qui contient toutes les implémentations de la stratégie possibles/connues/intéressantes (pour l'instant). Les implémentations enum constants évitent la multiplication de singletons. Ces enums sont typiquement stateless.

BinaryOperation.java
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
package cla.enums.patterns.strategy.complete;
 
public interface BinaryOperation {
 
    double apply(double d1, double d2);
 
    public enum ArithmeticOperation implements BinaryOperation {
 
        PLUS("+") {
            @Override public double apply(double d1, double d2) {
                return d1 + d2;
            }
        },
 
        MINUS("-") {
            @Override public double apply(double d1, double d2) {
                return d1 - d2;
            }
        },
 
        TIMES("*") {
            @Override public double apply(double d1, double d2) {
                return d1 * d2;
            }
        },
 
        DIVIDED_BY("/") {
            @Override public double apply(double d1, double d2) {
                return d1 / d2;
            }
        },
 
        ;
 
        private final String symbol;
        private ArithmeticOperation (String symbol) {this.symbol = symbol;}
 
        @Override public String toString() {
            return this.symbol;
        }
    }
 
}
 
public class BinaryOperationTest {
 
    @Test public void plus() {
        assertEquals(
                3D, 
                PLUS.apply(1D, 2D),
                0D
        );
    }
 
    @Test public void minus() {
        assertEquals(
                -1D, 
                MINUS.apply(1D, 2D),
                0D
        );
    }
 
    @Test public void times() {
        assertEquals(
                6D, 
                TIMES.apply(2D, 3D),
                0D
        );
    }
 
    @Test public void dividedBy() {
        assertEquals(
                2D, 
                DIVIDED_BY.apply(6D, 3D),
                0D
        );
    }
 
    @Test public void printPlus() {
        assertEquals(
                "+", 
                PLUS.toString()
        );
    }
 
    @Test public void printMinus() {
        assertEquals(
                "-", 
                MINUS.toString()
        );
    }
 
    @Test public void printTimes() {
        assertEquals(
                "*", 
                TIMES.toString()
        );
    }
 
    @Test public void printDividedBy() {
        assertEquals(
                "/", 
                DIVIDED_BY.toString()
        );
    }
}

V-B. Variation : implémentation partielle

Dans ce pattern, il existe d'autres implémentations, non-enums. Les implémentations enum constants évitent la multiplication de singletons. Ces enums sont typiquement stateless, alors que les implémentations non-enums ne le sont pas forcément (ex. une implémentation avec cache).

Étant données les deux classes :

Record.java
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
/**
 * Une entité du domaine
 */
public class Record {
 
    public boolean  logicallyDeleted; 
 
    //Exemple peu réaliste mais simple ; 
    //dans la réalité la suppression physique ne serait sûrement pas implémentée par un flag mais par une suppression du disque/BD.
    public boolean physicallyDeleted;
 
}
PhysicalDeleter.java
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
public interface RecordEnv {
 
    public interface PhysicalDeleter {
        void deletePhysically(Record r);
    }
    public interface LogicalDeleter {
        void deleteLogically(Record r);
    }
 
    default PhysicalDeleter physicalDeleter() {
        return (Record r) -> r.physicallyDeleted=true;
    }
    default LogicalDeleter logicalDeleter() {
        return (Record r) -> r.logicallyDeleted=true;
    }
 
}

Dans ce contexte, on peut avantageusement représenter certaines stratégies d'effacement d'une ligne par des enums :

RecordDeletionStrategy.java
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
public interface RecordDeletionStrategy {
 
    /**
     * L'unique méthode, ce qui est le plus fréquent pour une stratégie
     */
    void deleteRecord(Record r, RecordEnvironment env);
 
    /**
     * Les "enums dégénérées" sont une sous-variante du pattern,
     * dans laquelle les enum constants représentent des implémentations non opérationnelles,
     * utilisables comme Null Object (Fowler, POEA) ou comme fake object de test.
     * Quelques exemples de ces fakes objects:
     */
    public enum DegenerateImpls implements RecordDeletionStrategy {
 
        /**
         * Null object/dummy. 
         */
        NOOP {
            @Override public void deleteRecord(Record r, RecordEnvironment env) {
                ;
            }
        },
 
        /**
         * Implémentation par défaut d'une opération optionnelle (cf. le Collections framework du JDK).
         */
        UNSUPPORTIVE {
            @Override public void deleteRecord(Record r, RecordEnvironment env) {
                throw new UnsupportedOperationException();
            }
        },
 
        /**
         * Simule un problème, pour les tests de gestion d'erreur par exemple.
         */
        DEFECTIVE {
            @Override public void deleteRecord(Record r, RecordEnvironment env) {
                throw new RuntimeException();
            }
        },
 
        ;
    }
 
    /**
     * Les "implémentation connues" sont une autre sous-variante du pattern,
     *  les enum constants représentent des implémentations opérationnelles fournies par l'API,
     * mais qui peuvent être complétées par des implémentations de l'application.
     */
    public enum WellKnownImpls implements RecordDeletionStrategy {
 
        /**
         * Fait un delete en BD (par ex.)
         */
        PHYSICAL_DELETION {
            @Override public void deleteRecord(Record r, RecordEnvironment env) {
                env.physicalDeleter().deletePhysically(r);
            }
        },
 
        /**
         * Met le flag 'DELETED' à true 
         */
        LOGICAL_DELETION {
            @Override public void deleteRecord(Record r, RecordEnvironment env) {
                env.logicalDeleter().deleteLogically(r);
            }
        },
 
        ;
    }
}

V-C. Variation : enums du domaine avec inversion de dépendance

DI veut dire « dependency injection », mais est aussi parfois utilisé comme abréviation de « dependency inversion ». L'inversion de dépendance consiste à faire dépendre la couche technique de la couche métier au lieu de l'inverse.

Ceci permet d'introduire de façon non intrusive les préoccupations techniques dans l'application (une autre approche est l'AOP mais cette dernière est plus lourde à mettre en place). Le but de cette approche est d'éviter de polluer le code métier avec des préoccupations techniques (on constate souvent que celles-ci ont tendance à envahir le code supposément métier, jusqu'à en constituer la majorité). Un autre objectif est d'éviter les dépendances croisées entre les packages métier et les packages techniques.

En effet, la dépendance des packages métier vers les packages techniques est à priori un obstacle au Domain Driven Design qui préconise de mettre le plus possible de logique métier dans les classes qui représentent des entités du domaine. L'inversion de dépendance permet de contourner cet obstacle en introduisant dans la couche métier une abstraction, significative du point de vue de la couche métier, des services techniques.

Cette abstraction doit :

  • être dans le même package que les entités métier, pour éviter une dépendance de packages métier-->technique ;
  • avoir un nom métier et non technique, pour ne pas introduire de dépendance conceptuelle métier-->technique.

Exemple : dans le package model, Item peut dépendre de l'abstraction nommée Items, ItemsEnvironnement, ItemsRepository, mais pas de ItemsDB.

Plus précisément, le package métier com.toto.modele contient XXXEntity et XXXEnvironment/XXXs/XXXRepository (où XXX représente des trains, des entrepôts de carottes…), et le package technique contient l'implémentation com.toto.services contient XXXServicesImpl (par ex. un bean Spring transactionnel). La dépendance est donc bien dans le sens technique-->métier.

Prenons le cas de deux entités du modèle du domaine :

Item.java
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
package cla.enums.patterns.with.di.model;
/**
 * Une entité du domaine.
 * Ici son seul intérêt dans l'exemple est d'être agrégée par <tt>Bill</tt>. 
 */
//@Entity
public class Item {
    public long priceWithoutShipping;
    public double weight;
}
 
/**
 * Une entité du domaine.
 * <p>
 * Elle colocalise état et logique métier,
 * contrairement à l'antipattern fréquent des "entités étiques" (squelettiques).
 * Le livre "domain-driven design" décrit cet antipattern dans lequel
 * les entités ressemblent à des structs sans logique métier,
 * laquelle est alors principalement contenue dans des "transaction scripts"
 * (typiquement de longues méthodes procédurales dans un composant "frontière",
 * comme un web service, un EJB remote, un bean Spring transactionnel...)
 * </p>
 * <p>
 * L'inconvénient principal de cet antipattern est que comme la couche objet n'a pas d'épaisseur,
 * on perd tous les avantages de la modélisation objet.
 * </p>
 * <p>
 * Le livre explique en outre que la fréquence de cet antipattern s'explique 
 * par une difficulté technique : la plupart du temps, pour réaliser une logique métier non triviale,
 * les objets du domaine auraient besoin de dépendre de composants techniques ou frontière
 * (frontière au sens de boundary du pattern EE entity/control/boundary).
 * C'est  que réside la difficulté, car d'autre part les composants frontière dépendent toujours 
 * des entités du domaine; on aboutirait alors à une dépendance croisée entre les packages
 * <tt>cla.enums.patterns.with.polymorphism.and.di.model</tt> et 
 * <tt>cla.enums.patterns.with.polymorphism.and.di.services</tt>.
 * La façon la plus simple ___mais pas la meilleure___ de s'affranchir de cette difficulté
 * est de mettre toute la logique métier dans les composants frontière,
 * et on arrive alors à l'antipattern décrit (des transaction scripts de 300 lignes).     
 * </p>
 * <p>
 * Une solution plus satisfaisante est d'utiliser l'inversion de dépendance, 
 * qui consiste à remplacer la dépendance entre deux packages par une dépendance
 * des deux envers une abstraction commune, contenue dans le package métier
 * (ici cette abstraction est ItemsEnvironment).
 * </p>
 * <p>
 * En particulier en Java, on est souvent amené à utiliser cette technique 
 * avec des enums polymorphiques dont les constantes représentent les 
 * implémentations d'une Stratégie (GOF), ici ClientType.  
 * </p>       
 */
//@Entity //Les entités JPA sont un exemple typique d'entités du domaine. 
public class Bill {
 
    //@Field
    Item billedItem;
 
    //@Field, les enums sont persistées sans problème par JPA 
    // (de préference par leur name et non leur ordinal)
    ClientType billedClientType;
 
    //Implémente une logique métier non triviale,
    //mais pour ce faire à besoin d'un service externe technique/frontière.
    //Ici la logique métier est localisée dans l'enum "Stratégie" ClientType.
    public long computeTotalPrice(ItemsEnvironment env) {
        return billedItem.priceWithoutShipping + this.billedClientType.shippingFeeForWeight(
            this.billedItem.weight, 
            env
        );
    }
}

L'entité Bill implémente computeTotalPrice, car cela permet (conformément au DDD et au principe GRASP d'attribution de responsabilité « expert en information ») d'attribuer la responsabilité de produire une information agrégée à l'entité qui contient l'information détaillée. Mais ce calcul nécessite une dépendance envers un service technique (DAO, lookup…). Au lieu de renoncer au DDD et d'implémenter computeTotalPrice sous forme de code procédural dans un service frontière, on introduit une abstraction de ces services. Cette abstraction doit avoir un sens pour le domaine, et doit le moins possible évoquer une préoccupation technique (si possible pas du tout, pour éviter d'introduire une « dépendance croisée conceptuelle »).

Les enums étant supportées au niveau langage, elles sont faciles à sérialiser/persister en BD ou autre. Elles permettent ainsi, à moindres frais, de représenter le lien d'une entité du domaine vers une stratégie. Le besoin d'inversion de dépendance est localisé dans cette enum, dans la dépendance de la méthode shippingFeeForWeight envers un ItemsEnvironment.

ClientType.java
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
package cla.enums.patterns.with.di.model;
 
/**
 * Comme expliqué dans <tt>Bill</tt>, en Java on utilise souvent l'inversion de 
 * dépendance dans des enums polymorphiques et en particulier celles dont 
 * les constantes représentent les implémentations d'une Stratégie (GOF)
 * Ici shippingFeeForWeight dépend de l'abstraction <tt>ItemsEnvironment</tt> qui est dans 
 * le package <tt>model</tt>   
 * */
public enum ClientType {
 
    GOLD {
        @Override public long feeMultiplier() {
            return 1;
        }
    },
 
    SILVER {
        @Override public long feeMultiplier() {
            return 2;
        }
    },
 
    ;
 
    public long shippingFeeForWeight(double weight, ItemsEnvironment env) {
        //C'est ici qu'on a besoin d'un composant "service", 
        // mais on pourrait aussi dépendre d'un Repository, ...
        //Cette nécessité se manifeste souvent quand on veut faire du domain-driven design.
        long baseFee = env.fees().baseShippingFee(weight);
        return baseFee * feeMultiplier();
    }
 
    protected abstract long feeMultiplier();
}

L'abstraction de l'environnement/du contexte dans lequel baigne l'entité à l'allure suivante : elle contient typiquement une méthode de service, ou (comme ici) un ou plusieurs getters vers des interfaces de services.

ItemsEnvironment.java
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
package cla.enums.patterns.with.di.model;
 
 
/**
 * L'abstraction commune dont dépendent : 
 * <ul>
 * <li>les entités du domaine et en particulier l'enum <tt>ClientType</tt>
 * <li>le bean d'implémentation technique du package <tt>services</tt>, <tt>ItemsEnvironmentBean</tt>
 * </ul>
 */
public interface ItemsEnvironment {
 
    /**
     * Ici l'environnement ne comporte qu'un service technique. 
     * En réalité l'interface environnement pourrait exposer différents types de 
     * composants stateless ou stateful sous une forme abstraite pour la couche métier.
     * FeeServices représente le frais de port "de base" avant application 
     * d'un multiplicateur spécifique au type de client SILVER/GOLD.
     */
    FeeServices fees();
 
    /**
     * L'abstraction d'un service technique (par exemple un WS)
     */
    public interface FeeServices {
        long baseShippingFee(double weight);
    }
}

L'implémentation de l'environnement est contenue dans un package de service/frontière. La dépendance de packages est unidirectionnelle (service/frontière dépend de domaine).

ItemEnvironmentBean.java
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
package cla.enums.patterns.with.di.services;
 
/**
 * Bean Spring/EE dont ne veulent surtout pas dépendre les entités du domaine.
 * (elles ne dépendent que de ItemsEnvironment qui fait bien partie du package model)
 * */
public class ItemEnvironmentBean implements cla.enums.patterns.with.di.model.ItemsEnvironment {
 
    /**
     * Ici le côté technique de la classe d'implémentation est illustré par une dépendance 
     * à un service supposément technique, comme un WS remote par ex. 
     * On le met public pour simuler un @Inject de framework à peu de frais depuis ItemsTest#setup().  
     * */
    //@Inject
    public FeeServices feeServices;
 
    @Override public FeeServices fees() {
        return feeServices;
    }
 
}

Le test :

ItemsTest.java
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
package cla.enums.patterns.with.di.model;
 
import cla.enums.patterns.with.di.services.ItemEnvironmentBean;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
 
/**
 * Tests unitaires de la couche métier.
 */
public class ItemsTest {
 
    //@Inject bean technique (injection par Spring Test par ex., on peut facilement utiliser un "fake object"/mock)
    private ItemEnvironmentBean env;
 
    //Le SUT (System Under Test)
    private Bill bill;
 
    @Test public void totalPriceTakesBasePriceAndShippingFeeIntoAccount() {
        assertEquals(
            19.0D, //7 + (3*2)*2 
            bill.computeTotalPrice(this.env),
            0.0D //précision de double OK pour les petits entiers
        );
    }
 
    //---setup/teardown
    @Before public void setup() {
        //1. Create bill
        this.bill = new Bill();
        bill.billedItem = new Item();
        bill.billedItem.priceWithoutShipping = 7L;
        bill.billedItem.weight = 2.0D;
        bill.billedClientType = ClientType.SILVER;//*2
 
        //2. Simule l'injection de dépendance
        env = new ItemEnvironmentBean();
        env.feeServices = (double weight) -> 3*Math.round(weight);//3*2=6
    }
    @After public void teardown() {
        this.env = null;
        this.bill = null;
    }
}

VI. Conclusion et remerciements

En Java, on a la chance que les enums soient de vrais objets, alors autant les intégrer à nos designs. Comme pour beaucoup de patterns (Command par exemple), l'inversion de dépendance permet d'utiliser les enums dans une conception « Domain Driven ». Le code est téléchargeable à l'adresse https://github.com/vandekeiser/javaenums-implpatterns

Cet article a été publié avec l'aimable autorisation de Laurent Claisse. L'article original (Quelques patterns d'implémentation avec les enums java) peut être vu sur le blog/site de Zenika, experts en technologies Open Source et méthodes Agiles.

Nous tenons à remercier f-leb pour sa relecture orthographique attentive de cet article et Mickael Baron pour la mise au gabarit.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2015 Laurent Claisse. 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.