Utiliser des énumérations pour gérer des états plutôt que des booléens.

author
Andréas Hanss · Apr 1, 2022
dev | 5 min
Image descriptive

Les problèmes quand on essaye de gérer un état composite avec des booléens

Essayer de gérer l'état d'une fonction par l'utilisation de booléens pose plusieurs problèmes fondamentaux. Le premier est souvent appelé "explosion booléenne". Pour chaque booléen que nous ajoutons à une fonction, nous augmentons le nombre d'états possibles à un taux de 2^n où n est le nombre de booléens. En faisant le calcul quelques fois seulement, on obtient rapidement une quantité absurde d'états.

Le deuxième problème est que beaucoup de ces états sont des "états impossibles", des états dans lesquels notre application ne devrait jamais se trouver. Par exemple si on gère un état pour un animal avec un booléen estVivant est un autre estEntrainDabboyer on se rend compte qu'on ne peut pas être mort et être entrain d'aboyer en même temps. Pourtant au fur-et-à-mesure que le nombre de booléen s'enchainent et complexifie notre état, il y a de plus en plus de chances qu'à un moment on se retrouve dans un état comme celui-ci, ce qui ne devrait pas arriver.

Pour résoudre ce problème une manière saine est d'énumérer les états possibles et les évènement qui permettre de transitionner d'un état à un autre de manière déclarative, plutôt que de manière impérative.

Ce qui suit m'a pris quelques années à apprendre et surtout comprendre et m'a valu nombre d'essais et d'erreurs dans les logiciels que j'ai conçus. Je vais tenter de vous l'illustrer avec un exemple : Imaginons un logiciel d'achat de Pizzas, on choisit une quantité puis on clique sur « commander

Une approche naïve avec React serait d'avoir un formulaire de la sorte où l'ont gère la quantité et l'état d'envoi avec deux booléens.

1
const Pizza: React.FC = () => {
2
const [pizzaCount, setPizzaCount] = useState<number>(0);
3
const [isSent, setSent] = useState<boolean>(false);
4
5
return !isSent ? (
6
<form>
7
<input type="number" value={pizzaCount} onChange={(e) => setPizzaCount(e.target.valueAsNumber)} />
8
<button onClick={() => setSent(true)}>Envoyer</button>
9
</form>
10
) : (
11
<div>{pizzaCount} Pizzas under delivery</div>
12
);
13
};

Par la suite on nous demande de gérer des erreurs, puis comme le traitement du formulaire prends un certains temps, on nous demande de gérer un état de soumission du formulaire qui désactive le bouton pendant le chargement.

Nous sommes donc à 3 états booléens, soit 2^3 états possibles, soit 8 états possibles ce qui commence à faire.

Comme vous pouvez le voir, cela devient très verbeux et les possibilités d'erreurs et d'oublies sont assez probables.

1
import React, { useState } from "react";
2
3
const Pizza: React.FC = () => {
4
const [quantity, setQuantity] = useState<number>(0);
5
const [isSuccess, setSuccess] = useState<boolean>(false);
6
const [isSubmitting, setSubmitting] = useState<boolean>(false);
7
const [error, setError] = useState<string | null>(null);
8
9
const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => {
10
e.preventDefault();
11
setSubmitting(true);
12
13
if (error) {
14
setError(null);
15
}
16
17
sendOrder({ quantity })
18
.then((response) => {
19
console.log(response);
20
setSuccess(true);
21
setSubmitting(false);
22
})
23
.catch((err) => {
24
setError(err);
25
setSuccess(false); // just to be sure!
26
setSubmitting(false);
27
});
28
};
29
30
return !isSuccess ? (
31
<form onSubmit={handleSubmit}>
32
<input type="number" min={0} step={1} value={quantity} onChange={(e) => setQuantity(e.target.valueAsNumber)} />
33
<button disabled={isSubmitting} type="submit">
34
Envoyer
35
</button>
36
</form>
37
) : (
38
<div>
39
<div>{quantity} Pizzas under delivery</div>
40
<button
41
type="button"
42
onClick={() => {
43
setQuantity(0);
44
setSubmitting(false);
45
setSuccess(false);
46
}}
47
>
48
Reset
49
</button>
50
</div>
51
);
52
};
53
54
export default Pizza;

Mais, à ce stade, nous devons nous poser une question : est-ce qu'on veut vraiment être le prochain développeur à travailler sur ce formulaire ?

Je ne pense pas. Le code est complexe. Il y a des états à gauche et à droite. Il y a même des paramètres à des endroits où nous ne sommes même pas sûrs d'en avoir besoin. Et nous essayons d'éviter de mettre notre composant dans des états impossibles.

On peut constater que le nombre d'États commence à augmenter très rapidement. Mais avons-nous réellement autant d'états dans notre programme ? Notre formulaire utilise actuellement trois booléens, mais a-t-il vraiment huit états ? Non, la plupart de ces états sont impossibles. Par exemple, nous ne pouvons pas être dans un état où isSuccess, error et isSubmitting deviennent tous vrais. C'est impossible.

Alors pourquoi donnons-nous à nos programmes la possibilité d'être dans des états impossibles ?

Parce que nous n'énumérons pas.

C'est quoi l'énumération d'états et comment faire ?

L'énumération, qui consiste à identifier un ensemble de choses une par une, est un moyen simple mais efficace de rendre nos programmes un peu plus robustes.

Compte tenu des mêmes exigences, on rend compte que notre formulaire ne peut se trouver que dans une poignée d'états. Énumérons-les :

1
import React, { useState } from "react";
2
3
const Pizza: React.FC = () => {
4
const [quantity, setQuantity] = useState<number>(0);
5
const [currentState, setCurrentState] = React.useState<StatesEnum>(StatesEnum.Idle);
6
7
const send = (event: EventsEnum) => {
8
setCurrentState((current) => transition(current, event));
9
};
10
11
const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => {
12
e.preventDefault();
13
send(EventsEnum.SUBMIT);
14
15
sendOrder({ quantity })
16
.then((response) => {
17
console.log(response);
18
send(EventsEnum.SUCCEED);
19
})
20
.catch((err) => {
21
send(EventsEnum.FAIL);
22
});
23
};
24
25
return currentState !== StatesEnum.Success ? (
26
<form onSubmit={handleSubmit}>
27
<h2>Get my pizzas !</h2>
28
<input type="number" min={0} step={1} value={quantity} onChange={(e) => setQuantity(e.target.valueAsNumber)} />
29
<button disabled={currentState === StatesEnum.Submitting} type="submit">
30
Envoyer
31
</button>
32
{currentState === StatesEnum.Failure ? <div className="error_wrap">Failed to order pizzas</div> : null}
33
</form>
34
) : (
35
<div>
36
<div>{quantity} Pizzas under delivery</div>
37
<button
38
type="button"
39
onClick={() => {
40
send(EventsEnum.RESET);
41
}}
42
>
43
Reset
44
</button>
45
</div>
46
);
47
};
48
49
export default Pizza;
50
51
enum StatesEnum {
52
Idle,
53
Submitting,
54
Success,
55
Failure,
56
}
57
58
enum EventsEnum {
59
SUBMIT,
60
SUCCEED,
61
FAIL,
62
RESET,
63
}
64
65
const TRANSITIONS: Record<StatesEnum, Partial<Record<EventsEnum, StatesEnum>>> = {
66
[StatesEnum.Idle]: {
67
[EventsEnum.SUBMIT]: StatesEnum.Submitting,
68
},
69
[StatesEnum.Submitting]: {
70
[EventsEnum.SUCCEED]: StatesEnum.Success,
71
[EventsEnum.FAIL]: StatesEnum.Failure,
72
},
73
[StatesEnum.Success]: {
74
[EventsEnum.RESET]: StatesEnum.Idle,
75
},
76
[StatesEnum.Failure]: {
77
[EventsEnum.SUBMIT]: StatesEnum.Submitting,
78
},
79
};
80
81
const transition = (state: StatesEnum, event: EventsEnum) => {
82
const nextState = TRANSITIONS[state][event];
83
return nextState ? nextState : state;
84
};

Remarquons à quel point il est simple d'envoyer des événements et de s'assurer que notre composant est dans le bon état. Nous avons également beaucoup moins de conditions et de guards qu'auparavant. En éliminant l'existence d'états impossibles, nous avons créé un code dans lequel nous pouvons avoir une grande confiance.

Maintenant reposons-nous la question « souhaite-t-on vraiment être le prochain développeur à travailler sur ce formulaire ? »

En ce qui me concerne la réponse serait oui, mais bien évidemment il existe des outils qui nous permettre d'encore mieux représenter la gestion de nos états. Cela est présenté dans un autre article qui explique comment gérer l'état d'un jeu mobile à 3 étapes par tours en utilisant la librairie X-State