Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Implémentation d’un patron de conception orienté-objet

Le patron état est un patron de conception orienté objet. Le point essentiel de ce patron est que nous définissons un ensemble d’états qu’une valeur peut possède en interne. Les états sont représentés par un ensemble d’objets état, et le comportement de la valeur change en fonction de son état. Nous allons voir, à travers un exemple d’une structure de billet de blog qui a un champ contenant son état, lequel sera un objet état appartenant à l’ensemble “brouillon”, “en révision”, ou “publié”.

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’avantage d’utiliser le patron état est que, lorsque les exigences métier du programme changent, 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.

Nous allons commencer par implémenter le patron état dans mode plus traditionnel, orienté objet. Puis nous utiliserons une approche un peu plus naturelle en Rust. Voyons comment implémenter progressivement le déroulement d’un billet de blog en utilisant le patron état.

Les fonctionnalités finales du blog seront les suivantes :

  1. Un billet de blog commence par un brouillon vide.
  2. Lorsque le brouillon est terminé, une relecture du billet est demandée.
  3. Lorsqu’un billet est approuvé, il est publié.
  4. 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é.

Tentative de style orienté-objet traditionnel

Il y a une infinité de manières d’organiser du code pour résoudre un seul problème, chacune faisant des compromis différents. L’implémentation présentée dans cette section est plus dans un style orienté-objet traditionnel, qu’il est possible d’écrire en Rust, mais qui ne tire pas parti de certains des atouts de Rust. Plus tard, nous présenterons une autre solution utilisant toujours le modèle de conception orienté objet mais qui est structuré d’une manière qui pourrait sembler moins familière aux programmeurs ayant une expérience de programmation orientée objet. Nous comparerons les deux solutions afin de mettre en évidence les compromis liés à la conception de code Rust d’une manière différente du code dans d’autres langages.

L’encart 18-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.

Filename: 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());
}
Listing 18-11: Code that demonstrates the desired behavior we want our blog crate to have

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 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

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 18-12. Nous allons aussi créer un trait privé Etat qui définira le comportement de tous les objets état qu’un Billet se doit d’avoir.“

Ensuite, Billet devra avoir un objet trait de Box<dyn Etat> à l’intérieur d’une Option<T> dans un champ privé etat pour y loger l’objet état. Nous verrons plus tard pourquoi Option<T> est nécessaire.

Filename: 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 {}
Listing 18-12: Definition of a Post struct and a new function that creates a new Post instance, a State trait, and a Draft struct

Le trait Etat définit le comportement partagé par plusieurs états de billet. Les objets états sont Brouillon, EnRelecture et Publier, et ils vont tous implémenter le trait Etat. Pour l’instant, le trait n’a aucune 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.

Stockage du texte du contenu du billet

Nous avons vu dans l’encart 18-11 que nous souhaitons appeler une méthode ajouter_texte et lui passer un &str qui est ensuite ajouté comme le 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, de sorte 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 18-13.

Filename: 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 {}
Listing 18-13: Implementing the add_text method to add text to a post’s content

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 le premier assert_eq! de l’encart 18-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 18-14 montre l’implémentation de ceci.

Filename: 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 {}
Listing 18-14: Adding a placeholder implementation for the content method on Post that always returns an empty string slice

Avec cette méthode contenu ajoutée, tout ce qu’il y a dans l’encart 18-11 fonctionne comme prévu jusqu’au premier assert_eq!.

Demande d’une relecture du billet, ce qui change 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 18-15 montre ce code.

Filename: 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
    }
}
Listing 18-15: Implementing request_review methods on Post and the State trait

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 ajoutons 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 retourne 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. À 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 18-11 fonctionne maintenant jusqu’au second appel à assert_eq! !

Ajout d’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 18-16.

Filename: 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
    }
}
Listing 18-16: Implementing the approve method on Post and the State trait

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 18-17.

Filename: 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
    }
}
Listing 18-17: Updating the content method on Post to delegate to a content method on State

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 la section “Les cas où vous avez plus d’informations que le compilateur” 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.

À 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 c’est ici que nous allons placer la logique qui déterminera quel contenu retourner en fonction de quel état nous avons, comme le montre l’encart 18-18.

Filename: 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
    }
}
Listing 18-18: Adding the content method to the State trait

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. Bien que ce soit pratique, le fait que la méthode contenu de Etat détermine le contenu du Billet rend floue les limites entre la responsabilité de Etat et celle de Billet.

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 18-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.

Et pourquoi pas une énumération ?

Vous vous demandez peut-être pourquoi nous n’avons pas utilisé une énumération avec les différents états possibles des billets en tant que variantes. C’est certainement là une solution possible ; essayez de faire ainsi et comparez les résultats finaux pour voir quelle solution vous préférez ! Un inconvénient de l’utilisation d’une énumération est que chaque endroit qui va vérifier la valeur de l’énumération devra utiliser une expression match ou équivalent pour prendre en compte chaque variante possible. Ceci pourrait devenir plus répétitif que cette solution de l’objet trait.

Évaluation 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 par 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.

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 à un seul endroit.

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 de EnRelecture à Brouillon.
  • Attendre deux appels à approuver avant que l’état puisse être changé en Publier.
  • 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 de Billet.

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 ne fonctionnerait pas : lors de l’utilisation Etat en tant qu’objet trait, le trait ne sait pas ce qu’est exactement self, le type de retour n’est donc pas connu au moment de la compilation (c’est là l’une des règles de compatibilité dyn mentionnés plus haut).

Nous avons aussi des doublons dans le code des méthodes demander_relecture et approuver sur Billet. Ces deux méthodes utilisent Option::take avec le champ etat de Billet, et si etat est Some, elles délèguent leur travail à l’implémentation de la valeur encapsulée de la même méthode et assignent la nouvelle valeur du même champ etat au résultat. 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 aux macros dans le chapitre 20).

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 puisse 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 18-11 :

Filename: 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 18-19 nous propose les définitions d’une structure Billet et d’une structure BrouillonDeBillet ainsi que leurs méthodes.

Filename: 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);
    }
}
Listing 18-19: A Post with a content method and a DraftPost without a content method

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.

Donc, comment publier un billet ? Nous voulons faire respecter 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 18-20.

Filename: 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,
        }
    }
}
Listing 18-20: A PendingReviewPost that gets created by calling request_review on DraftPost and an approve method that turns a PendingReviewPost into a published Post

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 18-21.

Filename: 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 post = post.request_review();

    let post = post.approve();

    assert_eq!("J'ai mangé une salade au déjeuner aujourd'hui", billet.contenu());
}
Listing 18-21: Modifications to main to use the new implementation of the blog post workflow

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 18-21, 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 !