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▲
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 :
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 :
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 »).
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.0
D); //tan(pi/4)=1 cf. le cercle trigonométrique
public
static
double
cos
(
double
radians) {
return
0
D; //TODO
}
//Impossible à appeler
public
double
toto
(
) {
return
999
D;}
}
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 :
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.
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
(
3
D,
PLUS.apply
(
1
D, 2
D),
0
D
);
}
@Test
public
void
minus
(
) {
assertEquals
(
-
1
D,
MINUS.apply
(
1
D, 2
D),
0
D
);
}
@Test
public
void
times
(
) {
assertEquals
(
6
D,
TIMES.apply
(
2
D, 3
D),
0
D
);
}
@Test
public
void
dividedBy
(
) {
assertEquals
(
2
D,
DIVIDED_BY.apply
(
6
D, 3
D),
0
D
);
}
@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 :
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;
}
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 :
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,
* où 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 :
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 là 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.
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.
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).
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 :
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.0
D, //7 + (3*2)*2
bill.computeTotalPrice
(
this
.env),
0.0
D //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 =
7
L;
bill.billedItem.weight =
2.0
D;
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.