Implémenter un patron de conception orienté-objet
Le patron état est un patron de conception orienté objet. Le point essentiel de ce patron est qu'une valeur possède un état interne qui est représenté par un ensemble d'objets état, et le comportement de la valeur change en fonction de son état interne. Les objets état partagent des fonctionnalités : en Rust, bien sûr, nous utilisons des structures et des traits plutôt que des objets et de l'héritage. Chaque objet état est responsable de son propre comportement et décide lorsqu'il doit changer pour un autre état. La valeur contenue dans un objet état ne sait rien sur les différents comportements des états et ne sait pas quand il va changer d'état.
L'utilisation du patron état signifie que lorsque les exigences métier du programme ont changé, nous n'avons pas besoin de changer le code à l'intérieur de l'objet état ou le code qui utilise l'objet. Nous avons juste besoin de modifier le code dans un des objets état pour changer son fonctionnement ou pour ajouter d'autres objets état. Voyons un exemple du patron état et comment l'utiliser en Rust.
Nous allons implémenter un processus de publication de billets de blogs de manière incrémentale. Les fonctionnalités finales du blog seront les suivantes :
- Un billet de blog commence par un brouillon vide.
- Lorsque le brouillon est terminé, une relecture du billet est demandée.
- Lorsqu'un billet est approuvé, il est publié.
- Seuls les billets de blog publiés retournent du contenu à afficher si bien que les billets non approuvés ne peuvent pas être publiés accidentellement.
Tous les autres changements effectués sur un billet n'auront pas d'effet. Par exemple, si nous essayons d'approuver un brouillon de billet de blog avant d'avoir demandé une relecture, le billet devrait rester à l'état de brouillon non publié.
L'encart 17-11 présente ce processus de publication sous forme de code : c'est
un exemple d'utilisation de l'API que nous allons implémenter dans une crate de
bibliothèque blog
. Elle ne va pas encore se compiler car nous n'avons pas
encore implémenté la crate blog
.
Fichier : src/main.rs
use blog::Billet;
fn main() {
let mut billet = Billet::new();
billet.ajouter_texte("J'ai mangé une salade au déjeuner aujourd'hui");
assert_eq!("", billet.contenu());
billet.demander_relecture();
assert_eq!("", billet.contenu());
billet.approuver();
assert_eq!("J'ai mangé une salade au déjeuner aujourd'hui", billet.contenu());
}
Nous voulons permettre à l'utilisateur de créer un nouveau brouillon de billet
de blog avec Billet::new
. Nous voulons qu'il puisse ajouter du texte au
billet de blog. Si nous essayons d'obtenir immédiatement le contenu du billet,
avant qu'il ne soit relu, nous n'obtiendrons aucun texte car le billet est
toujours un brouillon. Nous avons ajouté des assert_eq!
dans le code pour les
besoins de la démonstration. Un excellent test unitaire pour cela serait de
vérifier qu'un brouillon de billet de blog retourne bien une chaîne de
caractères vide à partir de la méthode contenu
, mais nous n'allons pas écrire
de tests pour cet exemple.
Ensuite, nous voulons permettre de demander une relecture du billet, et nous
souhaitons que contenu
retourne toujours une chaîne de caractères vide pendant
que nous attendons la relecture. Lorsque la relecture du billet est approuvée,
il doit être publié, ce qui signifie que le texte du billet doit être retourné
lors de l'appel à contenu
.
Remarquez que le seul type avec lequel nous interagissons avec la crate est le
type Billet
. Ce type va utiliser le patron état et va héberger une valeur qui
sera un des trois objets état représentant les différents états par lesquels
passe un billet : brouillon, en attente de relecture ou publié. Le changement
d'un état à un autre sera géré en interne du type Billet
. Les états vont
changer en réponse à l'appel des méthodes de l'instance de Billet
par les
utilisateurs de notre bibliothèque qui n'auront donc pas à les gérer
directement. Ainsi les utilisateurs ne peuvent pas faire d'erreur avec les
états, comme celle de publier un billet avant qu'il ne soit relu par exemple.
Définir Billet
et créer une nouvelle instance à l'état de brouillon
Commençons l'implémentation de la bibliothèque ! Nous savons que nous aurons
besoin d'une structure publique Billet
qui héberge du contenu, donc nous
allons commencer par définir cette structure ainsi qu'une fonction publique
new
qui lui est associée pour créer une instance de Billet
, comme dans
l'encart 17-12. Nous allons aussi créer un trait privé Etat
. Ensuite Billet
devra avoir un champ privé etat
pour y loger une Option<T>
contenant un
objet trait de Box<dyn Etat>
. Nous verrons plus tard l'intérêt du Option<T>
.
Fichier : src/lib.rs
pub struct Billet {
etat: Option<Box<dyn Etat>>,
contenu: String,
}
impl Billet {
pub fn new() -> Billet {
Billet {
etat: Some(Box::new(Brouillon {})),
contenu: String::new(),
}
}
}
trait Etat {}
struct Brouillon {}
impl Etat for Brouillon {}
Le trait Etat
définit le comportement partagé par plusieurs états de billet,
et les états Brouillon
, EnRelecture
et Publier
vont tous implémenter ce
trait Etat
. Pour l'instant, le trait n'a pas de méthode, et nous allons
commencer par définir uniquement l'état Brouillon
car c'est l'état dans lequel
nous voulons que soit un nouveau billet lorsqu'il est créé.
Lorsque nous créons un nouveau Billet
, nous assignons à son champ etat
une
valeur Some
qui contient une Box
. Cette Box
pointe sur une nouvelle
instance de la structure Brouillon
. Cela garantira qu'à chaque fois que nous
créons une nouvelle instance de Billet
, elle commencera à l'état de brouillon.
Comme le champ etat
de Billet
est privé, il n'y a pas d'autre manière de
créer un Billet
dans un autre état ! Dans la fonction Billet::new
, nous
assignons une nouvelle String
vide au champ contenu
.
Stocker le texte du contenu du billet
L'encart 17-11 a montré que nous souhaitons appeler une méthode ajouter_texte
et lui passer un &str
qui est ensuite ajouté au contenu textuel du billet de
blog. Nous implémentons ceci avec une méthode plutôt que d'exposer publiquement
le champ contenu
avec pub
. Cela signifie que nous pourrons implémenter une
méthode plus tard qui va contrôler comment le champ contenu
sera lu. La
méthode ajouter_texte
est assez simple, donc ajoutons son implémentation dans
le bloc Billet
de l'encart 17-13 :
Fichier : src/lib.rs
pub struct Billet {
etat: Option<Box<dyn Etat>>,
contenu: String,
}
impl Billet {
// -- partie masquée ici --
pub fn new() -> Billet {
Billet {
etat: Some(Box::new(Brouillon {})),
contenu: String::new(),
}
}
pub fn ajouter_texte(&mut self, texte: &str) {
self.contenu.push_str(texte);
}
}
trait Etat {}
struct Brouillon {}
impl Etat for Brouillon {}
La méthode ajouter_texte
prend en argument une référence mutable vers self
,
car nous changeons l'instance Billet
sur laquelle nous appelons
ajouter_texte
. Nous faisons ensuite appel à push_str
sur le String
dans
contenu
et nous y envoyons l'argument texte
pour l'ajouter au contenu
déjà
stocké. Ce comportement ne dépend pas de l'état dans lequel est le billet, donc
cela ne fait pas partie du patron état. La méthode ajouter_texte
n'interagit
pas du tout avec le champ etat
, mais c'est volontaire.
S'assurer que le contenu d'un brouillon est vide
Même si nous avons appelé ajouter_texte
et ajouté du contenu dans notre
billet, nous voulons que la méthode contenu
retourne toujours une slice de
chaîne de caractères vide car le billet est toujours à l'état de brouillon,
comme le montre la ligne 7 de l'encart 17-11. Implémentons maintenant la méthode
contenu
de la manière la plus simple qui réponde à cette consigne : toujours
retourner un slice de chaîne de caractères vide. Nous la changerons plus tard
lorsque nous implémenterons la capacité de changer l'état d'un billet afin qu'il
puisse être publié. Pour l'instant, les billets ne peuvent qu'être à l'état de
brouillon, donc le contenu du billet devrait toujours être vide. L'encart 17-14
montre l'implémentation de ceci :
Fichier : src/lib.rs
pub struct Billet {
etat: Option<Box<dyn Etat>>,
contenu: String,
}
impl Billet {
// -- partie masquée ici --
pub fn new() -> Billet {
Billet {
etat: Some(Box::new(Brouillon {})),
contenu: String::new(),
}
}
pub fn ajouter_texte(&mut self, texte: &str) {
self.contenu.push_str(texte);
}
pub fn contenu(&self) -> &str {
""
}
}
trait Etat {}
struct Brouillon {}
impl Etat for Brouillon {}
Avec cette méthode contenu
ajoutée, tout ce qu'il y a dans l'encart 17-11
fonctionne comme prévu jusqu'à la ligne 7.
Demander une relecture du billet va changer son état
Ensuite, nous avons besoin d'ajouter une fonctionnalité pour demander la
relecture d'un billet, qui devrait changer son état de Brouillon
à
EnRelecture
. L'encart 17-15 montre ce code :
Fichier : src/lib.rs
pub struct Billet {
etat: Option<Box<dyn Etat>>,
contenu: String,
}
impl Billet {
// -- partie masquée ici --
pub fn new() -> Billet {
Billet {
etat: Some(Box::new(Brouillon {})),
contenu: String::new(),
}
}
pub fn ajouter_texte(&mut self, texte: &str) {
self.contenu.push_str(texte);
}
pub fn contenu(&self) -> &str {
""
}
pub fn demander_relecture(&mut self) {
if let Some(s) = self.etat.take() {
self.etat = Some(s.demander_relecture())
}
}
}
trait Etat {
fn demander_relecture(self: Box<Self>) -> Box<dyn Etat>;
}
struct Brouillon {}
impl Etat for Brouillon {
fn demander_relecture(self: Box<Self>) -> Box<dyn Etat> {
Box::new(EnRelecture {})
}
}
struct EnRelecture {}
impl Etat for EnRelecture {
fn demander_relecture(self: Box<Self>) -> Box<dyn Etat> {
self
}
}
Nous installons la méthode publique demander_relecture
sur Billet
qui va
prendre en argument une référence mutable à self
. Ensuite nous appelons la
méthode interne demander_relecture
sur l'état interne de Billet
, et cette
deuxième méthode demander_relecture
consomme l'état en cours et applique un
nouvel état.
Nous avons ajouté la méthode demander_relecture
sur le trait Etat
; tous les
types qui implémentent le trait vont maintenant devoir implémenter la méthode
demander_relecture
. Remarquez qu'au lieu d'avoir self
, &self
, ou
&mut self
en premier paramètre de la méthode, nous avons self: Box<Self>
.
Cette syntaxe signifie que la méthode est valide uniquement lorsqu'on l'appelle
sur une Box
qui contient ce type. Cette syntaxe prend possession de
Box<Self>
, ce qui annule l'ancien état du Billet
qui peut changer pour un
nouvel état.
Pour consommer l'ancien état, la méthode demander_relecture
a besoin de
prendre possession de la valeur d'état. C'est ce à quoi sert le Option
dans le
champ etat
de Billet
: nous faisons appel à la méthode take
pour obtenir
la valeur dans le Some
du champ etat
et le remplacer par None
, car Rust ne
nous permet pas d'avoir des champs non renseignés dans des structures. Cela nous
permet d'extraire la valeur de etat
d'un Billet
, plutôt que de l'emprunter.
Ensuite, nous allons réaffecter le résultat de cette opération à etat
du
Billet
concerné.
Nous devons assigner temporairement None
à etat
plutôt que de lui donner
directement avec du code tel que self.etat = self.etat.demander_relecture();
car
nous voulons prendre possession de la valeur etat
. Cela garantit que Billet
ne peut pas utiliser l'ancienne valeur de etat
après qu'on ait changé cet état.
La méthode demander_relecture
sur Brouillon
doit retourner une nouvelle
instance d'une structure EnRelecture
dans une Box
, qui représente l'état
lorsqu'un billet est en attente de relecture. La structure EnRelecture
implémente elle aussi la méthode demander_relecture
mais ne fait aucune
modification. A la place, elle se retourne elle-même, car lorsque nous demandons
une relecture sur un billet déjà à l'état EnRelecture
, il doit rester à l'état
EnRelecture
.
Désormais nous commençons à voir les avantages du patron état : la méthode
demander_relecture
sur Billet
est la même peu importe la valeur de son
etat
. Chaque état est maître de son fonctionnement.
Nous allons conserver la méthode contenu
sur Billet
comme elle est, elle
va donc continuer à retourner une slice de chaîne de caractères vide. Nous pouvons
maintenant avoir un Billet
à l'état Brouillon
ou EnRelecture
, mais nous
voulons qu'il suive le même comportement lorsqu'il est dans l'état
EnRelecture
. L'encart 17-11 fonctionne maintenant jusqu'à la ligne 10 !
Ajouter une méthode approuver
qui change le comportement de contenu
La méthode approuver
ressemble à la méthode demander_relecture
: elle va
changer etat
pour lui donner la valeur que l'état courant retournera lorsqu'il
sera approuvé, comme le montre l'encart 17-16 :
Fichier : src/lib.rs
pub struct Billet {
etat: Option<Box<dyn Etat>>,
contenu: String,
}
impl Billet {
// -- partie masquée ici --
pub fn new() -> Billet {
Billet {
etat: Some(Box::new(Brouillon {})),
contenu: String::new(),
}
}
pub fn ajouter_texte(&mut self, texte: &str) {
self.contenu.push_str(texte);
}
pub fn contenu(&self) -> &str {
""
}
pub fn demander_relecture(&mut self) {
if let Some(s) = self.etat.take() {
self.etat = Some(s.demander_relecture())
}
}
pub fn approuver(&mut self) {
if let Some(s) = self.etat.take() {
self.etat = Some(s.approuver())
}
}
}
trait Etat {
fn demander_relecture(self: Box<Self>) -> Box<dyn Etat>;
fn approuver(self: Box<Self>) -> Box<dyn Etat>;
}
struct Brouillon {}
impl Etat for Brouillon {
// -- partie masquée ici --
fn demander_relecture(self: Box<Self>) -> Box<dyn Etat> {
Box::new(EnRelecture {})
}
fn approuver(self: Box<Self>) -> Box<dyn Etat> {
self
}
}
struct EnRelecture {}
impl Etat for EnRelecture {
// -- partie masquée ici --
fn demander_relecture(self: Box<Self>) -> Box<dyn Etat> {
self
}
fn approuver(self: Box<Self>) -> Box<dyn Etat> {
Box::new(Publier {})
}
}
struct Publier {}
impl Etat for Publier {
fn demander_relecture(self: Box<Self>) -> Box<dyn Etat> {
self
}
fn approuver(self: Box<Self>) -> Box<dyn Etat> {
self
}
}
Nous avons ajouté la méthode approuver
au trait Etat
et ajouté une nouvelle
structure Publier
, qui implémente Etat
.
Comme pour la façon de fonctionner de demander_relecture
sur EnRelecture
,
si nous faisons appel à la méthode approuver
sur un Brouillon
, cela n'aura
pas d'effet car approuver
va retourner self
. Lorsque nous appellerons
approuver
sur EnRelecture
, elle va retourner une nouvelle instance de la
structure Publier
dans une instance de Box
. La structure Publier
implémente le trait Etat
, et pour chacune des méthodes demander_relecture
et approuver
, elle va retourner elle-même, car le billet doit rester à l'état
Publier
dans ce cas-là.
Nous devons maintenant modifier la méthode contenu
sur Billet
. Nous
souhaitons que la valeur retournée par contenu
dépende de l'état actuel du
Billet
, donc nous allons faire en sorte que le Billet
délègue sa logique à
une méthode contenu
défini sur son etat
, comme dans l'encart 17-17 :
Fichier : src/lib.rs
pub struct Billet {
etat: Option<Box<dyn Etat>>,
contenu: String,
}
impl Billet {
// -- partie masquée ici --
pub fn new() -> Billet {
Billet {
etat: Some(Box::new(Brouillon {})),
contenu: String::new(),
}
}
pub fn ajouter_texte(&mut self, texte: &str) {
self.contenu.push_str(texte);
}
pub fn contenu(&self) -> &str {
self.etat.as_ref().unwrap().contenu(self)
}
// -- partie masquée ici --
pub fn demander_relecture(&mut self) {
if let Some(s) = self.etat.take() {
self.etat = Some(s.demander_relecture())
}
}
pub fn approuver(&mut self) {
if let Some(s) = self.etat.take() {
self.etat = Some(s.approuver())
}
}
}
trait Etat {
fn demander_relecture(self: Box<Self>) -> Box<dyn Etat>;
fn approuver(self: Box<Self>) -> Box<dyn Etat>;
}
struct Brouillon {}
impl Etat for Brouillon {
fn demander_relecture(self: Box<Self>) -> Box<dyn Etat> {
Box::new(EnRelecture {})
}
fn approuver(self: Box<Self>) -> Box<dyn Etat> {
self
}
}
struct EnRelecture {}
impl Etat for EnRelecture {
fn demander_relecture(self: Box<Self>) -> Box<dyn Etat> {
self
}
fn approuver(self: Box<Self>) -> Box<dyn Etat> {
Box::new(Publier {})
}
}
struct Publier {}
impl Etat for Publier {
fn demander_relecture(self: Box<Self>) -> Box<dyn Etat> {
self
}
fn approuver(self: Box<Self>) -> Box<dyn Etat> {
self
}
}
Comme notre but est de conserver toutes ces règles dans les structures qui
implémentent Etat
, nous appelons une méthode contenu
sur la valeur de
etat
et nous lui passons en argument l'instance du billet (avec le self
).
Nous retournons ensuite la valeur retournée par la méthode contenu
sur la
valeur de etat
.
Nous faisons appel à la méthode as_ref
sur Option
car nous voulons une
référence vers la valeur dans Option
plutôt que d'en prendre possession. Comme
etat
est un Option<Box<dyn Etat>>
, lorsque nous faisons appel à as_ref
,
une Option<&Box<dyn Etat>>
est retournée. Si nous n'avions pas fait appel à
as_ref
, nous aurions obtenu une erreur car nous ne pouvons pas déplacer
etat
de &self
, lui-même est emprunté et provenant des paramètres de la fonction.
Nous faisons ensuite appel à la méthode unwrap
, mais nous savons qu'elle ne
va jamais paniquer, car nous savons que les méthodes sur Billet
vont garantir
que etat
contiendra toujours une valeur Some
lorsqu'elles seront utilisées.
C'est un des cas dont nous avons parlé dans
une section du chapitre 9 lorsque nous
savions qu'une valeur None
ne serait jamais possible, même si le compilateur
n'est pas capable de le comprendre.
A partir de là, lorsque nous faisons appel à contenu
sur &Box<dyn Etat>
,
l'extrapolation de déréférencement va s'appliquer sur le &
et le Box
pour
que la méthode contenu
puisse finalement être appelée sur le type qui
implémente le trait Etat
. Cela signifie que nous devons ajouter contenu
à la
définition du trait Etat
, et que c'est ici que nous allons placer la logique
pour le contenu à retourner en fonction de l'état nous avons, comme le montre
l'encart 17-18 :
Fichier : src/lib.rs
pub struct Billet {
etat: Option<Box<dyn Etat>>,
contenu: String,
}
impl Billet {
pub fn new() -> Billet {
Billet {
etat: Some(Box::new(Brouillon {})),
contenu: String::new(),
}
}
pub fn ajouter_texte(&mut self, texte: &str) {
self.contenu.push_str(texte);
}
pub fn contenu(&self) -> &str {
self.etat.as_ref().unwrap().contenu(self)
}
pub fn demander_relecture(&mut self) {
if let Some(s) = self.etat.take() {
self.etat = Some(s.demander_relecture())
}
}
pub fn approuver(&mut self) {
if let Some(s) = self.etat.take() {
self.etat = Some(s.approuver())
}
}
}
trait Etat {
// -- partie masquée ici --
fn demander_relecture(self: Box<Self>) -> Box<dyn Etat>;
fn approuver(self: Box<Self>) -> Box<dyn Etat>;
fn contenu<'a>(&self, billet: &'a Billet) -> &'a str {
""
}
}
// -- partie masquée ici --
struct Brouillon {}
impl Etat for Brouillon {
fn demander_relecture(self: Box<Self>) -> Box<dyn Etat> {
Box::new(EnRelecture {})
}
fn approuver(self: Box<Self>) -> Box<dyn Etat> {
self
}
}
struct EnRelecture {}
impl Etat for EnRelecture {
fn demander_relecture(self: Box<Self>) -> Box<dyn Etat> {
self
}
fn approuver(self: Box<Self>) -> Box<dyn Etat> {
Box::new(Publier {})
}
}
struct Publier {}
impl Etat for Publier {
// -- partie masquée ici --
fn demander_relecture(self: Box<Self>) -> Box<dyn Etat> {
self
}
fn approuver(self: Box<Self>) -> Box<dyn Etat> {
self
}
fn contenu<'a>(&self, billet: &'a Billet) -> &'a str {
&billet.contenu
}
}
Nous avons ajouté une implémentation par défaut pour la méthode contenu
qui
retourne une slice de chaîne de caractères vide. Cela nous permet de ne pas
avoir à implémenter contenu
sur les structures Brouillon
et EnRelecture
.
La structure Publier
va remplacer la méthode contenu
et retourner la valeur
présente dans billet.contenu
.
Remarquez aussi que nous devons annoter des durées de vie sur cette méthode,
comme nous l'avons vu au chapitre 10. Nous allons prendre en argument une
référence au billet
et retourner une référence à une partie de ce billet
,
donc la durée de vie retournée par la référence est liée à la durée de vie de
l'argument billet
.
Et nous avons maintenant terminé, tout le code de l'encart 17-11 fonctionne
désormais ! Nous avons implémenté le patron état avec les règles de notre
processus de publication définies pour notre blog. La logique des règles est
intégrée dans les objets état plutôt que d'être dispersée un peu partout dans
Billet
.
Les inconvénients du patron état
Nous avons démontré que Rust est capable d'implémenter le patron état qui est
orienté objet pour regrouper les différents types de comportement qu'un billet
doit avoir à chaque état. Les méthodes sur Billet
ne savent rien des
différents comportements. De la manière dont nous avons organisé le code, nous
n'avons qu'à regarder à un seul endroit pour connaître les différents
comportements qu'un billet publié va suivre : l'implémentation du trait Etat
sur la structure Publier
.
Si nous avions utilisé une autre façon d'implémenter ces règles sans utiliser
le patron état, nous aurions dû utiliser des expressions match
dans les
méthodes de Billet
ou même dans le code du main
qui vérifie l'état du
billet et les comportements associés aux changements d'états. Cela aurait eu
pour conséquence d'avoir à regarder à différents endroits pour comprendre toutes
les conséquences de la publication d'un billet ! Et ce code grossirait au fur et
à mesure que nous ajouterions des états : chaque expression match
devrait avoir
des nouvelles branches pour ces nouveaux états.
Avec le patron état, les méthodes de Billet
et les endroits où nous utilisons
Billet
n'ont pas besoin d'expressions match
, et pour ajouter un nouvel état,
nous avons seulement besoin d'ajouter une nouvelle structure et d'implémenter
les méthodes du trait sur cette structure.
L'implémentation qui utilise le patron état est facile à améliorer pour ajouter plus de fonctionnalités. Pour découvrir la simplicité de maintenance du code qui utilise le patron état, essayez d'accomplir certaines de ces suggestions :
- Ajouter une méthode
rejeter
qui fait retourner l'état d'un billet deEnRelecture
àBrouillon
. - Attendre deux appels à
approuver
avant que l'état puisse être changé enPublier
. - Permettre aux utilisateurs d'ajouter du contenu textuel uniquement lorsqu'un
billet est à l'état
Brouillon
. Indice : rendre l'objet état responsable de ce qui peut changer dans le contenu mais pas responsable de la modification deBillet
.
Un inconvénient du patron état est que comme les états implémentent les
transitions entre les états, certains des états sont couplés entre eux. Si nous
ajoutons un nouvel état entre EnRelecture
et Publier
, Planifier
par exemple,
nous devrons alors changer le code dans EnRelecture
pour qu'il passe ensuite
à l'état Planifier
au lieu de Publier
. Cela représenterait moins de travail
si EnRelecture
n'avait pas besoin de changer lorsqu'on ajoute un nouvel état, mais
cela signifierait alors qu'il faudrait changer de patron.
Un autre inconvénient est que nous avons de la logique en double. Pour éviter ces
doublons, nous devrions essayer de faire en sorte que les méthodes
demander_relecture
et approuver
qui retournent self
deviennent les
implémentations par défaut sur le trait Etat
; cependant, cela violerait la
sûreté des objets, car le trait ne sait pas ce qu'est exactement self
. Nous
voulons pouvoir utiliser Etat
en tant qu'objet trait, donc nous avons besoin
que ses méthodes soient sûres pour les objets.
Nous avons aussi des doublons dans le code des méthodes demander_relecture
et
approuver
sur Billet
. Ces deux méthodes délèguent leur travail à la même
méthode de la valeur du champ etat
de type Option
et assignent la nouvelle
valeur du même champ etat
à la fin. Si nous avions beaucoup de méthodes sur
Billet
qui suivaient cette logique, nous devrions envisager de définir une
macro pour éviter cette répétition (voir la
section dédiée dans le chapitre 19).
En implémentant le patron état exactement comme il est défini pour les
langages orientés-objet, nous ne profitons pas pleinement des avantages de
Rust. Voyons voir si nous pouvons faire quelques changements pour que la crate
blog
puisse lever des erreurs dès la compilation lorsqu'elle aura détecté des
états ou des transitions invalides.
Implémenter les états et les comportements avec des types
Nous allons vous montrer comment repenser le patron état pour qu'il offre des compromis différents. Plutôt que d'encapsuler complètement les états et les transitions, faisant que le code externe ne puissent pas les connaître, nous allons coder ces états sous forme de différents types. En conséquence, le système de vérification de type de Rust va empêcher toute tentative d'utilisation des brouillons de billets là où seuls des billets publiés sont autorisés, en provoquant une erreur de compilation.
Considérons la première partie du main
de l'encart 17-11 :
Fichier : src/main.rs
use blog::Billet;
fn main() {
let mut billet = Billet::new();
billet.ajouter_texte("J'ai mangé une salade au déjeuner aujourd'hui");
assert_eq!("", billet.contenu());
billet.demander_relecture();
assert_eq!("", billet.contenu());
billet.approuver();
assert_eq!("J'ai mangé une salade au déjeuner aujourd'hui", billet.contenu());
}
Nous pouvons toujours créer de nouveaux billets à l'état de brouillon en
utilisant Billet::new
et ajouter du texte au contenu du billet. Mais au lieu
d'avoir une méthode contenu
sur un brouillon de billet qui retourne une chaîne
de caractères vide, nous faisons en sorte que les brouillons de billets n'aient
même pas de méthode contenu
. Ainsi, si nous essayons de récupérer le contenu
d'un brouillon de billet, nous obtenons une erreur de compilation qui nous
informera que la méthode n'existe pas. Finalement, il nous sera impossible de
publier le contenu d'un brouillon de billet en production, car ce code ne se
compilera même pas. L'encart 17-19 nous propose les définitions d'une structure
Billet
et d'une structure BrouillonDeBillet
ainsi que leurs méthodes :
Fichier : src/lib.rs
pub struct Billet {
contenu: String,
}
pub struct BrouillonDeBillet {
contenu: String,
}
impl Billet {
pub fn new() -> BrouillonDeBillet {
BrouillonDeBillet {
contenu: String::new(),
}
}
pub fn contenu(&self) -> &str {
&self.contenu
}
}
impl BrouillonDeBillet {
pub fn ajouter_texte(&mut self, texte: &str) {
self.contenu.push_str(texte);
}
}
Les deux structures Billet
et BrouillonDeBillet
ont un champ privé
contenu
qui stocke le texte du billet de blog. Les structures n'ont plus le
champ etat
car nous avons déplacé la signification de l'état directement dans
le nom de ces types de structures. La structure Billet
représente un billet
publié et possède une méthode contenu
qui retourne le contenu
.
Nous avons toujours la fonction Billet::new
, mais au lieu de retourner une
instance de Billet
, elle va retourner une instance de BrouillonDeBillet
.
Comme contenu
est privé et qu'il n'y a pas de fonction qui retourne Billet
,
il ne sera pas possible pour le moment de créer une instance de Billet
.
La structure BrouillonDeBillet
a une méthode ajouter_texte
, donc nous
pouvons ajouter du texte à contenu
comme nous le faisions avant, mais
remarquez toutefois que BrouillonDeBillet
n'a pas de méthode contenu
de
définie ! Donc pour l'instant le programme s'assure que tous les billets
démarrent à l'état de brouillon et que les brouillons ne proposent pas de
contenu à publier. Toute tentative d'outre-passer ces contraintes va
déclencher une erreur de compilation.
Implémenter les changements d'état en tant que changement de type
Donc, comment publier un billet ? Nous voulons renforcer la règle qui dit qu'un
brouillon de billet doit être relu et approuvé avant de pouvoir être publié. Un
billet à l'état de relecture doit continuer à ne pas montrer son contenu.
Implémentons ces contraintes en introduisant une nouvelle structure,
BilletEnRelecture
, en définissant la méthode demander_relecture
sur
BrouillonDeBillet
retournant un BilletEnRelecture
, et en définissant une
méthode approuver
sur BilletEnRelecture
pour qu'elle retourne un Billet
,
comme le propose l'encart 17-20 :
Fichier : src/lib.rs
pub struct Billet {
contenu: String,
}
pub struct BrouillonDeBillet {
contenu: String,
}
impl Billet {
pub fn new() -> BrouillonDeBillet {
BrouillonDeBillet {
contenu: String::new(),
}
}
pub fn contenu(&self) -> &str {
&self.contenu
}
}
impl BrouillonDeBillet {
// -- partie masquée ici --
pub fn ajouter_texte(&mut self, texte: &str) {
self.contenu.push_str(texte);
}
pub fn demander_relecture(self) -> BilletEnRelecture {
BilletEnRelecture {
contenu: self.contenu,
}
}
}
pub struct BilletEnRelecture {
contenu: String,
}
impl BilletEnRelecture {
pub fn approuver(self) -> Billet {
Billet {
contenu: self.contenu,
}
}
}
Les méthodes demander_relecture
et approuver
prennent possession de self
,
ce qui consomme les instances de BrouillonDeBillet
et de BilletEnRelecture
pour les transformer respectivement en BilletEnRelecture
et en Billet
.
Ainsi, il ne restera plus d'instances de BrouillonDeBillet
après avoir appelé
approuver
sur elles, et ainsi de suite. La structure BilletEnRelecture
n'a
pas de méthode contenu
qui lui est définie, donc si on essaye de lire son
contenu, on obtient une erreur de compilation, comme avec BrouillonDeBillet
.
Comme la seule manière d'obtenir une instance de Billet
qui a une méthode
contenu
de définie est d'appeler la méthodeapprouver
sur un
BilletEnRelecture
, et que la seule manière d'obtenir un BilletEnRelecture
est d'appeler la méthode demander_relecture
sur un BrouillonDeBillet
, nous
avons désormais intégré le processus de publication des billets de blog avec le
système de type.
Mais nous devons aussi faire quelques petits changements dans le main
. Les
méthodes demander_relecture
et approuver
retournent des nouvelles instances
au lieu de modifier la structure sur laquelle elles ont été appelées, donc nous
devons ajouter des assignations de masquage let billet =
pour stocker les
nouvelles instances retournées. Nous ne pouvons pas non plus vérifier que le
contenu des brouillons de billets et de ceux en cours de relecture sont bien
vides, donc nous n'avons plus besoin des vérifications associées : en effet,
nous ne pouvons plus compiler du code qui essaye d'utiliser le contenu d'un
billet dans ces états. Le code du main
mis à jour est présenté dans
l'encart 17-21 :
Fichier : src/main.rs
use blog::Billet;
fn main() {
let mut billet = Billet::new();
billet.ajouter_texte("J'ai mangé une salade au déjeuner aujourd'hui");
let billet = billet.demander_relecture();
let billet = billet.approuver();
assert_eq!("J'ai mangé une salade au déjeuner aujourd'hui", billet.contenu());
}
Les modifications que nous avons eu besoin de faire à main
pour réassigner
billet
impliquent que cette implémentation ne suit plus exactement le patron
état orienté-objet : les changements d'états ne sont plus totalement intégrés
dans l'implémentation de Billet
. Cependant, nous avons obtenu que les
états invalides sont désormais impossibles grâce au système de types et à la
vérification de type qui s'effectue à la compilation ! Cela garantit que certains
bogues, comme l'affichage du contenu d'un billet non publié, seront détectés avant
d'arriver en production.
Essayez d'implémenter les exigences fonctionnelles supplémentaires suggérées
dans la liste présente au début de cette section,
sur la crate blog
dans l'état où elle était après l'encart 17-20, afin de
vous faire une idée sur cette façon de concevoir le code. Notez aussi que
certaines de ces exigences pourraient déjà être implémentées implicitement du
fait de cette conception.
Nous avons vu que même si Rust est capable d'implémenter des patrons de conception orientés-objet, d'autres patrons, tel qu'intégrer l'état dans le système de type, sont également possibles en Rust. Ces patrons présentent différents avantages et inconvénients. Bien que vous puissiez être très familier avec les patrons orientés-objet, vous gagnerez à repenser les choses pour tirer avantage des fonctionnalités de Rust, telles que la détection de certains bogues à la compilation. Les patrons orientés-objet ne sont pas toujours la meilleure solution en Rust à cause de certaines de ses fonctionnalités, comme la possession, que les langages orientés-objet n'ont pas.
Résumé
Que vous pensiez ou non que Rust est un langage orienté-objet après avoir lu ce chapitre, vous savez maintenant que vous pouvez utiliser les objets trait pour pouvoir obtenir certaines fonctionnalités orienté-objet en Rust. La répartition dynamique peut offrir de la flexibilité à votre code en échange d'une perte de performances à l'exécution. Vous pouvez utiliser cette flexibilité pour implémenter des patrons orientés-objet qui facilitent la maintenance de votre code. Rust offre d'autres fonctionnalités, comme la possession, que les langages orientés-objet n'ont pas. L'utilisation d'un patron orienté-objet n'est pas toujours la meilleure manière de tirer parti des avantages de Rust, mais cela reste une option disponible.
Dans le chapitre suivant, nous allons étudier les motifs, qui constituent une autre des fonctionnalités de Rust et apportent beaucoup de flexibilité. Nous les avons abordés brièvement dans le livre, mais nous n'avons pas encore vu tout leur potentiel. C'est parti !