Un cas d’utilisation: Enumérations avancées
#
Nous avons vu dans une section précédente (Enum
) que les énumérations Java ne permettaient pas d’avoir des valeurs de structures différentes. Avec la théorie que nous avons abordé jusqu’à présent, il devient possible d’écrire une énumération avancée à l’aide de l’héritage et des classes imbriquées.
Nous souhaitons réaliser une structure équivalente au langage Rust
:
enum Status {
Cancelled { date: Date },
OutOfOrder { error_message: String },
Running
}
qui se traduit en BNF par:
Status ::= Cancelled(Date) | OutOfOrder(String) | Running
C’est-à-dire qu’un statut peut prendre trois états différents. Si une fonction nous retourne un statut, il devient important de savoir dans quel état il se trouve. Voici un exemple d’utilisation:
public static Status random() { ... }
...
Status s = random();
if (s.isRunning()) {
System.out.println( "running..." );
} else if (s.isCancelled()) {
System.out.println( "cancelled at " + s.cancellationDate() );
} else {
System.out.println( "Out of order: " + s.errorMessage() );
}
Contrairement à la volonté de permettre l’extension d’un type, nous souhaitons ici la contrôler. Tout comme une énumération, les valeurs de l’énumération doivent être connues à l’avance. Nous allons utiliser plusieurs techniques vues jusqu’ici:
- Le polymorphisme: un référence de type
Status
peut pointer sur un des trois sous-types
- L’héritage: les trois sous-types héritent de
Status
- Les classes internes: les trois sous-types sont internes à
Status
- Les fonctions anonymes pour rendre notre design plus déclaratif
Première esquisse
#
La première esquisse est la suivante:
1
2
3
4
5
6
7
8
9
10
11
|
public interface Status {
static class Running implements Status {
}
static class Cancelled implements Status {
public LocalDate cancellationDate() { ... }
}
static class OutOfOrder implements Status {
public String errorMessage() { ... }
}
}
|
Le défaut est de devoir vérifier le sous-type pour connaître l’état et de caster ensuite la classe pour appeler la méthode spécifique à l’état:
1
2
3
4
|
Status s = random();
if (s instanceof Status.OutOfOrder ) {
String info = ((OutOfOrder)s).errorMessage();
}
|
ce qui est une très mauvaise pratique. Nous allons donc mettre à disposition des méthodes qui permettent d’interroger l’objet pour connaître son état sans avoir à utiliser d’opérateur instanceof
, de méthodes getClass
et de cast. Nous souhaitons utiliser une référence de la sorte:
1
2
3
4
5
6
7
8
|
Status s = random();
if (s.isRunning()) {
System.out.println( "running..." );
} else if (s.isCancelled()) {
System.out.println( "cancelled at " + s.cancellationDate() );
} else {
System.out.println( "Out of order: " + s.errorMessage() );
}
|
Deuxième esquisse
#
Pour réaliser ceci, les méthodes isRunning
, isCancelled
et évenuellement isOutOfOrder
doivent être proposées par l’interface Status
et doivent donc être redéfinie par les sous-types, tout comme cancellationDate
et errorMessage
:
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
|
public interface Status {
static class Running implements Status {
public boolean isRunning() { return true; }
public boolean isCancelled() { return false; }
public boolean isOutOfOrder() { return false; }
public String errorMessage() {
throw new IllegalStateException("state is running correctly!");
}
public LocalDate cancellationDate() {
throw new IllegalStateException("state is running correctly!");
}
}
static class Cancelled implements Status {
private LocalDate date;
public Cancelled(LocalDate date) { this.date = date; }
public boolean isRunning() { return false; }
public boolean isCancelled() { return true; }
public boolean isOutOfOrder() { return false; }
public String errorMessage() {
throw new IllegalStateException("state is cancelled. Error message couldn't exist!");
}
public LocalDate cancellationDate() { return this.date; }
}
static class OutOfOrder implements Status {
private String message;
public OutOfOrder(String message) { this.message = message; }
public boolean isRunning() { return false; }
public boolean isCancelled() { return false; }
public boolean isOutOfOrder() { return true; }
public String errorMessage() { return this.message; }
public LocalDate cancellationDate() {
throw new IllegalStateException("out of order state. Error message couldn't exist");
}
}
boolean isRunning();
boolean isCancelled();
boolean isOutOfOrder();
String errorMessage();
LocalDate cancellationDate();
}
|
Troisième esquisse
#
Pour créer une instance d’un sous-type, il est nécessaire de l’écrire de la sorte:
Status s = new Status.Running();
Privilégions l’emploi de fabriques pour notre conception afin de permettre cette syntaxe:
Status s = Status.running();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public interface Status {
static class Running implements Status {
private Running() {}
...
}
static class Cancelled implements Status {
private Cancelled(LocalDate date) { this.date = date; }
...
}
static class OutOfOrder implements Status {
private OutOfOrder(String message) { this.message = message; }
...
}
...
static Status running() { return new Running(); }
static Status outOfOrderState(String error) { return new OutOfOrder(error); }
static Status cancelledState(LocalDate cancellationDate) { return new Cancelled(cancellationDate); }
}
|
Cette version nous permet d’interroger notre objet pour connaître son état avant d’appeler la bonne méthode. Cependant, la mauvaise manipulation peut entraîner une levée d’exception et ce design brise le princie Tell, don’t ask.
Quatrième esquisse
#
Regardons comment rendre notre design plus déclaratif. Nous souhaisons remplacer le code:
1
2
3
4
5
6
7
8
|
Status s = random();
if (s.isRunning()) {
System.out.println( "running..." );
} else if (s.isCancelled()) {
System.out.println( "cancelled at " + s.cancellationDate() );
} else {
System.out.println( "Out of order: " + s.errorMessage() );
}
|
par:
1
2
3
4
5
6
7
|
Status s = random();
s.apply(
() -> System.out.println("running...") ),
(LocalDate cancellationDate) -> System.out.println( "cancelled at " + cancellationDate ),
(String errorMessage) -> System.out.println( "Out of order: " + errorMessage )
);
|
La méthode apply
accepte trois actions en fonction du context.
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
|
public interface Status {
static class Running implements Status {
...
public void apply(Runnable ifRunning, Consumer<LocalDate> ifCancelled, Consumer<String> ifOut) {
ifRunning.run();
}
}
static class Cancelled implements Status {
private LocalDate date;
...
public void apply(Runnable ifRunning, Consumer<LocalDate> ifCancelled, Consumer<String> ifOut) {
ifCancelled.accept(this.date);
}
...
}
static class OutOfOrder implements Status {
private String message;
...
public void apply(Runnable ifRunning, Consumer<LocalDate> ifCancelled, Consumer<String> ifOut) {
ifOut.accept(message);
}
...
}
...
void apply(Runnable ifRunning, Consumer<LocalDate> ifCancelled, Consumer<String> ifOut);
}
|
Cette dernière version permet d’éviter de demander des informations au préalable, mais permet également d’éviter de gérer les exceptions en cas de mauvaise manipulation. Il serait possible de proposer uniquement la version déclarative à l’utilisateur.
La version finale se trouve ici
Exercice
#
La version actuelle de apply
nous oblige à passer les trois actions pour les trois états possibles. Modifiez le code pour permettre d’utiliser une méthode par action:
1
2
3
4
|
Status status = ...
status.ifRunning( () -> System.out.println("running...") );
.ifCancelled( cancellationDate -> System.out.println( "cancelled at " + cancellationDate )
.ifOutOfOrder( errorMessage -> System.out.println( "Out of order: " + errorMessage) );
|