Implémentation d'un patron de conception orienté objet

RustRustBeginner
Pratiquer maintenant

This tutorial is from open-source community. Access the source code

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Bienvenue dans Implementing an Object-Oriented Design Pattern. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous allons implémenter le patron d'état dans une conception orientée objet pour créer une structure de publication de blog qui passe par différents états (brouillon, en revue et publié) en fonction de son comportement, en veillant à ce que seule les publications de blog publiées puissent renvoyer du contenu.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100443{{"Implémentation d'un patron de conception orienté objet"}} rust/mutable_variables -.-> lab-100443{{"Implémentation d'un patron de conception orienté objet"}} rust/string_type -.-> lab-100443{{"Implémentation d'un patron de conception orienté objet"}} rust/function_syntax -.-> lab-100443{{"Implémentation d'un patron de conception orienté objet"}} rust/expressions_statements -.-> lab-100443{{"Implémentation d'un patron de conception orienté objet"}} rust/method_syntax -.-> lab-100443{{"Implémentation d'un patron de conception orienté objet"}} rust/traits -.-> lab-100443{{"Implémentation d'un patron de conception orienté objet"}} rust/operator_overloading -.-> lab-100443{{"Implémentation d'un patron de conception orienté objet"}} end

Implementing an Object-Oriented Design Pattern

Le patron d'état est un patron de conception orienté objet. Le cœur du patron est que nous définissons un ensemble d'états qu'une valeur peut avoir en interne. Les états sont représentés par un ensemble d'objets d'état, et le comportement de la valeur change en fonction de son état. Nous allons travailler sur un exemple d'une structure de publication de blog qui a un champ pour stocker son état, qui sera un objet d'état de l'ensemble "brouillon", "en revue" ou "publié".

Les objets d'état partagent des fonctionnalités : en Rust, bien sûr, nous utilisons des structs et des traits plutôt qu'objets et héritage. Chaque objet d'état est responsable de son propre comportement et de la gouvernance du moment où il devrait changer en un autre état. La valeur qui contient un objet d'état ne sait rien sur le comportement différent des états ou sur le moment de passer d'un état à l'autre.

L'avantage d'utiliser le patron d'état est que, lorsque les exigences commerciales du programme changent, nous n'aurons pas besoin de modifier le code de la valeur contenant l'état ou le code qui utilise la valeur. Nous n'aurons qu'à mettre à jour le code à l'intérieur d'un des objets d'état pour changer ses règles ou peut-être ajouter plus d'objets d'état.

Tout d'abord, nous allons implémenter le patron d'état d'une manière plus traditionnelle orientée objet, puis nous utiliserons une approche qui est un peu plus naturelle en Rust. Plongeons-nous pour implémenter progressivement un workflow de publication de blog en utilisant le patron d'état.

La fonctionnalité finale ressemblera à ceci :

  1. Une publication de blog commence comme un brouillon vide.
  2. Lorsque le brouillon est terminé, une révision de la publication est demandée.
  3. Lorsque la publication est approuvée, elle est publiée.
  4. Seules les publications de blog publiées renvoient du contenu pour imprimer, de sorte que les publications non approuvées ne peuvent pas être publiées par accident.

Toute autre modification tentée sur une publication ne devrait avoir aucun effet. Par exemple, si nous essayons d'approuver une publication de blog en brouillon avant d'avoir demandé une révision, la publication devrait rester un brouillon non publié.

Le Listing 17-11 montre ce workflow sous forme de code : c'est un exemple d'utilisation de l'API que nous allons implémenter dans une boîte à outils nommée blog. Cela ne compilera pas encore car nous n'avons pas implémenté la boîte à outils blog.

Nom de fichier : src/main.rs

use blog::Post;

fn main() {
  1 let mut post = Post::new();

  2 post.add_text("I ate a salad for lunch today");
  3 assert_eq!("", post.content());

  4 post.request_review();
  5 assert_eq!("", post.content());

  6 post.approve();
  7 assert_eq!("I ate a salad for lunch today", post.content());
}

Listing 17-11 : Code qui démontre le comportement souhaité que nous voulons que notre boîte à outils blog ait

Nous voulons autoriser l'utilisateur à créer une nouvelle publication de blog en brouillon avec Post::new [1]. Nous voulons autoriser le texte à être ajouté à la publication de blog [2]. Si nous essayons d'obtenir le contenu de la publication immédiatement, avant l'approbation, nous ne devrions pas obtenir de texte car la publication est toujours un brouillon. Nous avons ajouté assert_eq! dans le code à des fins de démonstration [3]. Un excellent test d'unité pour cela serait d'affirmer qu'une publication de blog en brouillon renvoie une chaîne de caractères vide à partir de la méthode content, mais nous ne allons pas écrire de tests pour cet exemple.

Ensuite, nous voulons autoriser une demande de révision de la publication [4], et nous voulons que content renvoie une chaîne de caractères vide pendant que la publication est en attente d'être revue [5]. Lorsque la publication reçoit l'approbation [6], elle devrait être publiée, ce qui signifie que le texte de la publication sera renvoyé lorsqu'on appelle content [7].

Remarquez que le seul type avec lequel nous interagissons à partir de la boîte à outils est le type Post. Ce type utilisera le patron d'état et contiendra une valeur qui sera l'un des trois objets d'état représentant les différents états dans lesquels une publication peut se trouver - brouillon, en revue ou publié. Passer d'un état à l'autre sera géré en interne dans le type Post. Les états changent en réponse aux méthodes appelées par les utilisateurs de notre bibliothèque sur l'instance Post, mais ils n'ont pas besoin de gérer directement les changements d'état. De plus, les utilisateurs ne peuvent pas se tromper sur les états, par exemple en publiant une publication avant qu'elle ne soit revue.

Définition de Post et création d'une nouvelle instance dans l'état brouillon

Commencons l'implémentation de la bibliothèque! Nous savons que nous avons besoin d'une structure publique Post qui contient du contenu, donc nous allons commencer par la définition de la structure et d'une fonction publique associée new pour créer une instance de Post, comme montré dans le Listing 17-12. Nous allons également créer un trait privé State qui définira le comportement que tous les objets d'état d'un Post doivent avoir.

Ensuite, Post contiendra un objet de trait Box<dyn State> dans un Option<T> dans un champ privé nommé state pour stocker l'objet d'état. Vous allez voir pourquoi l'Option<T> est nécessaire dans un instant.

Nom de fichier : src/lib.rs

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
          1 state: Some(Box::new(Draft {})),
          2 content: String::new(),
        }
    }
}

trait State {}

struct Draft {}

impl State for Draft {}

Listing 17-12 : Définition d'une structure Post et d'une fonction new qui crée une nouvelle instance de Post, d'un trait State et d'une structure Draft

Le trait State définit le comportement partagé par différents états de publication. Les objets d'état sont Draft, PendingReview et Published, et ils implémenteront tous le trait State. Pour l'instant, le trait n'a pas de méthodes, et nous allons commencer par définir seulement l'état Draft car c'est l'état dans lequel nous voulons que commencent les publications.

Lorsque nous créons une nouvelle instance de Post, nous définissons son champ state sur une valeur Some qui contient une Box [1]. Cette Box pointe vers une nouvelle instance de la structure Draft. Cela garantit que chaque fois que nous créons une nouvelle instance de Post, elle commencera comme un brouillon. Étant donné que le champ state de Post est privé, il n'est pas possible de créer un Post dans un autre état! Dans la fonction Post::new, nous définissons le champ content sur une nouvelle chaîne de caractères vide String [2].

Stockage du texte du contenu de la publication

Dans le Listing 17-11, nous avons vu que nous voulons être en mesure d'appeler une méthode nommée add_text et de lui passer une &str qui sera ensuite ajoutée comme contenu textuel de la publication de blog. Nous l'implémentons comme une méthode, plutôt que d'exposer le champ content en tant que pub, afin que plus tard nous puissions implémenter une méthode qui contrôlera la lecture des données du champ content. La méthode add_text est assez simple, donc ajoutons l'implémentation dans le Listing 17-13 au bloc impl Post.

Nom de fichier : src/lib.rs

impl Post {
    --snip--
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

Listing 17-13 : Implémentation de la méthode add_text pour ajouter du texte au content d'une publication

La méthode add_text prend une référence mutable à self car nous modifions l'instance de Post sur laquelle nous appelons add_text. Nous appelons ensuite push_str sur la String dans content et passons l'argument text pour l'ajouter au content enregistré. Ce comportement ne dépend pas de l'état de la publication, donc il n'est pas partie du patron d'état. La méthode add_text n'interagit pas du tout avec le champ state, mais elle fait partie du comportement que nous voulons prendre en charge.

Vérification que le contenu d'un brouillon de publication est vide

Même après avoir appelé add_text et ajouté du contenu à notre publication, nous voulons toujours que la méthode content renvoie une chaîne de caractères vide car la publication est toujours dans l'état brouillon, comme montré à [3] dans le Listing 17-11. Pour l'instant, implémentons la méthode content avec la chose la plus simple qui répondra à cette exigence : en renvoyant toujours une chaîne de caractères vide. Nous changerons cela plus tard une fois que nous aurons implémenté la capacité de changer l'état d'une publication pour qu'elle puisse être publiée. Jusqu'à présent, les publications ne peuvent être que dans l'état brouillon, donc le contenu de la publication devrait toujours être vide. Le Listing 17-14 montre cette implémentation provisoire.

Nom de fichier : src/lib.rs

impl Post {
    --snip--
    pub fn content(&self) -> &str {
        ""
    }
}

Listing 17-14 : Ajout d'une implémentation provisoire pour la méthode content sur Post qui renvoie toujours une chaîne de caractères vide

Avec cette méthode content ajoutée, tout dans le Listing 17-11 jusqu'à la ligne [3] fonctionne comme prévu.

Demande d'une révision qui change l'état de la publication

Ensuite, nous devons ajouter une fonctionnalité pour demander une révision d'une publication, ce qui devrait changer son état de Draft à PendingReview. Le Listing 17-15 montre ce code.

Nom de fichier : src/lib.rs

impl Post {
    --snip--
  1 pub fn request_review(&mut self) {
      2 if let Some(s) = self.state.take() {
          3 self.state = Some(s.request_review())
        }
    }
}

trait State {
  4 fn request_review(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
      5 Box::new(PendingReview {})
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
      6 self
    }
}

Listing 17-15 : Implémentation des méthodes request_review sur Post et le trait State

Nous donnons à Post une méthode publique nommée request_review qui prendra une référence mutable à self [1]. Ensuite, nous appelons une méthode interne request_review sur l'état actuel de Post [3], et cette deuxième méthode request_review consomme l'état actuel et renvoie un nouvel état.

Nous ajoutons la méthode request_review au trait State [4] ; tous les types qui implémentent le trait devront désormais implémenter la méthode request_review. Notez que plutôt que d'avoir self, &self ou &mut self comme premier paramètre de la méthode, nous avons self: Box<Self>. Cette syntaxe signifie que la méthode n'est valide que lorsqu'elle est appelée sur une Box contenant le type. Cette syntaxe prend la propriété de Box<Self>, invalidant l'ancien état afin que la valeur d'état de Post puisse se transformer en un nouvel état.

Pour consommer l'ancien état, la méthode request_review doit prendre la propriété de la valeur d'état. C'est là que l'Option dans le champ state de Post intervient : nous appelons la méthode take pour extraire la valeur Some du champ state et laisser un None à sa place car Rust ne nous permet pas d'avoir des champs non initialisés dans les structs [2]. Cela nous permet de déplacer la valeur state hors de Post plutôt que de la prêter. Ensuite, nous définirons la valeur d'état de la publication sur le résultat de cette opération.

Nous devons définir state sur None temporairement plutôt que de le définir directement avec du code comme self.state = self.state.request_review(); pour obtenir la propriété de la valeur state. Cela garantit que Post ne peut pas utiliser l'ancienne valeur d'état après avoir été transformé en un nouvel état.

La méthode request_review sur Draft renvoie une nouvelle instance emballée d'un nouveau struct PendingReview, qui représente l'état lorsqu'une publication est en attente d'une révision [5]. Le struct PendingReview implémente également la méthode request_review mais ne fait aucune transformation. Au contraire, il renvoie lui-même [6] car lorsqu'on demande une révision d'une publication déjà dans l'état PendingReview, elle devrait rester dans l'état PendingReview.

Maintenant, nous commençons à voir les avantages du patron d'état : la méthode request_review sur Post est la même peu importe sa valeur d'état. Chaque état est responsable de ses propres règles.

Nous laisserons la méthode content sur Post inchangée, renvoyant une chaîne de caractères vide. Maintenant, nous pouvons avoir un Post dans l'état PendingReview ainsi que dans l'état Draft, mais nous voulons le même comportement dans l'état PendingReview. Le Listing 17-11 fonctionne désormais jusqu'à la ligne [5]!

Ajout de la méthode approve pour modifier le comportement de content

La méthode approve sera similaire à la méthode request_review : elle définira state sur la valeur que l'état actuel indique qu'il devrait avoir lorsqu'il est approuvé, comme montré dans le Listing 17-16.

Nom de fichier : src/lib.rs

impl Post {
    --snip--
    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    --snip--
    fn approve(self: Box<Self>) -> Box<dyn State> {
      1 self
    }
}

struct PendingReview {}

impl State for PendingReview {
    --snip--
    fn approve(self: Box<Self>) -> Box<dyn State> {
      2 Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

Listing 17-16 : Implémentation de la méthode approve sur Post et le trait State

Nous ajoutons la méthode approve au trait State et ajoutons un nouveau struct qui implémente State, l'état Published.

De manière similaire à la façon dont request_review fonctionne sur PendingReview, si nous appelons la méthode approve sur un Draft, elle n'aura aucun effet car approve renverra self [1]. Lorsque nous appelons approve sur PendingReview, elle renvoie une nouvelle instance emballée du struct Published [2]. Le struct Published implémente le trait State, et pour les deux méthodes request_review et approve, il renvoie lui-même car la publication devrait rester dans l'état Published dans ces cas.

Maintenant, nous devons mettre à jour la méthode content sur Post. Nous voulons que la valeur renvoyée par content dépende de l'état actuel du Post, donc nous allons faire en sorte que Post délègue à une méthode content définie sur son state, comme montré dans le Listing 17-17.

Nom de fichier : src/lib.rs

impl Post {
    --snip--
    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }
    --snip--
}

Listing 17-17 : Mise à jour de la méthode content sur Post pour déléguer à une méthode content sur State

Comme l'objectif est de conserver toutes ces règles dans les structs qui implémentent State, nous appelons une méthode content sur la valeur dans state et passons l'instance de la publication (c'est-à-dire self) en tant qu'argument. Ensuite, nous renvoyons la valeur renvoyée par l'utilisation de la méthode content sur la valeur state.

Nous appelons la méthode as_ref sur l'Option car nous voulons une référence à la valeur à l'intérieur de l'Option plutôt que la propriété de la valeur. Étant donné que state est une Option<Box<dyn State>>, lorsqu'on appelle as_ref, une Option<&Box<dyn State>> est renvoyée. Si nous n'avions pas appelé as_ref, nous aurions eu une erreur car nous ne pouvons pas déplacer state hors de la référence empruntée &self du paramètre de fonction.

Nous appelons ensuite la méthode unwrap, que nous savons ne jamais déclencher une panique car nous savons que les méthodes sur Post garantissent que state contiendra toujours une valeur Some une fois que ces méthodes sont terminées. C'est l'un des cas dont nous avons parlé dans "Cas où vous avez plus d'informations que le compilateur" lorsque nous savons qu'une valeur None n'est jamais possible, même si le compilateur n'est pas capable de le comprendre.

À ce stade, lorsqu'on appelle content sur le &Box<dyn State>, la coercition de déréférencement prendra effet sur le & et la Box de sorte que la méthode content sera finalement appelée sur le type qui implémente le trait State. Cela signifie que nous devons ajouter content à la définition du trait State, et c'est là que nous mettrons la logique pour savoir quel contenu renvoyer selon l'état que nous avons, comme montré dans le Listing 17-18.

Nom de fichier : src/lib.rs

trait State {
    --snip--
    fn content<'a>(&self, post: &'a Post) -> &'a str {
      1 ""
    }
}

--snip--
struct Published {}

impl State for Published {
    --snip--
    fn content<'a>(&self, post: &'a Post) -> &'a str {
      2 &post.content
    }
}

Listing 17-18 : Ajout de la méthode content au trait State

Nous ajoutons une implémentation par défaut pour la méthode content qui renvoie une chaîne de caractères vide [1]. Cela signifie que nous n'avons pas besoin d'implémenter content sur les structs Draft et PendingReview. Le struct Published remplacera la méthode content et renverra la valeur dans post.content [2].

Notez que nous avons besoin d'annotations de durée de vie pour cette méthode, comme nous l'avons discuté au Chapitre 10. Nous prenons une référence à un post en tant qu'argument et renvoyons une référence à une partie de ce post, donc la durée de vie de la référence renvoyée est liée à la durée de vie de l'argument post.

Et nous avons terminé --- tout le Listing 17-11 fonctionne désormais! Nous avons implémenté le patron d'état avec les règles du workflow de publication de blog. La logique liée aux règles réside dans les objets d'état plutôt que d'être dispersée dans tout Post.

Pourquoi pas un enum?

Vous vous êtes peut-être demandé pourquoi nous n'avons pas utilisé un enum avec les différents états possibles de publication comme variantes. C'est certainement une solution possible ; essayez et comparez les résultats finaux pour voir lequel vous préférez! Un inconvénient d'utiliser un enum est que chaque endroit qui vérifie la valeur de l'enum devra avoir une expression match ou similaire pour gérer chaque variante possible. Cela pourrait être plus répétitif que cette solution d'objet de trait.

Choix de compromis du patron d'état

Nous avons montré que Rust est capable d'implémenter le patron d'état orienté objet pour encapsuler les différents types de comportement qu'une publication devrait avoir dans chaque état. Les méthodes sur Post ne connaissent rien des différents comportements. Avec la manière dont nous avons organisé le code, il suffit de regarder dans un seul endroit pour connaître les différents comportements d'une publication publiée : l'implémentation du trait State sur le struct Published.

Si nous créions une implémentation alternative qui n'utiliserait pas le patron d'état, nous pourrions plutôt utiliser des expressions match dans les méthodes de Post ou même dans le code de main qui vérifie l'état de la publication et change le comportement à ces endroits. Cela signifierait que nous devrions regarder dans plusieurs endroits pour comprendre toutes les implications d'une publication étant dans l'état publié! Cela ne ferait qu'augmenter avec le nombre d'états que nous ajouterions : chaque expression match aurait besoin d'un autre bras.

Avec le patron d'état, les méthodes de Post et les endroits où nous utilisons Post n'ont pas besoin d'expressions match, et pour ajouter un nouvel état, il suffirait d'ajouter un nouveau struct et d'implémenter les méthodes du trait sur ce seul struct.

L'implémentation utilisant le patron d'état est facile à étendre pour ajouter plus de fonctionnalités. Pour voir la simplicité de la maintenance du code utilisant le patron d'état, essayez quelques-unes de ces suggestions :

  • Ajoutez une méthode reject qui change l'état de la publication de PendingReview en retournant à Draft.
  • Exigez deux appels à approve avant que l'état ne puisse être changé en Published.
  • Autorisez les utilisateurs à ajouter du contenu textuel seulement lorsqu'une publication est dans l'état Draft. Indice : rendez l'objet d'état responsable de ce qui peut changer au sujet du contenu mais pas responsable de modifier Post.

Un inconvénient du patron d'état est que, parce que les états implémentent les transitions entre les états, certains des états sont couplés les uns aux autres. Si nous ajoutons un autre état entre PendingReview et Published, tel que Scheduled, nous devrons modifier le code dans PendingReview pour passer à Scheduled à la place. Il faudrait moins de travail si PendingReview n'avait pas besoin de changer avec l'ajout d'un nouvel état, mais cela signifierait passer à un autre patron de conception.

Un autre inconvénient est que nous avons dupliqué une certaine logique. Pour éliminer une partie de la duplication, nous pourrions essayer de créer des implémentations par défaut pour les méthodes request_review et approve sur le trait State qui renvoient self. Cependant, cela ne fonctionnerait pas : lorsqu'on utilise State comme un objet de trait, le trait ne sait pas exactement quel sera le self concret, de sorte que le type de retour n'est pas connu au moment de la compilation.

Une autre duplication inclut les implémentations similaires des méthodes request_review et approve sur Post. Les deux méthodes délèguent à l'implémentation de la même méthode sur la valeur dans le champ state de Option et définissent la nouvelle valeur du champ state sur le résultat. Si nous avions beaucoup de méthodes sur Post qui suivaient ce modèle, nous pourrions considérer définir une macro pour éliminer la répétition (voir "Macros").

En implémentant le patron d'état exactement comme il est défini pour les langages orientés objet, nous ne tirons pas pleinement parti des atouts de Rust comme nous le pourrions. Regardons quelques modifications que nous pouvons apporter au crate blog qui peuvent transformer les états invalides et les transitions en erreurs de compilation.

Encodage des états et du comportement sous forme de types

Nous allons vous montrer comment repenser le patron d'état pour obtenir un autre ensemble de choix de compromis. Au lieu d'encapsuler complètement les états et les transitions de sorte que le code externe n'en ait aucune connaissance, nous allons encoder les états dans différents types. En conséquence, le système de vérification de type de Rust empêchera les tentatives d'utilisation de publications brouillons là où seulement les publications publiées sont autorisées en émettant une erreur de compilation.

Considérons la première partie de main dans le Listing 17-11 :

Nom de fichier : src/main.rs

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());
}

Nous continuons à autoriser la création de nouvelles publications dans l'état brouillon en utilisant Post::new et la possibilité d'ajouter du texte au contenu de la publication. Mais au lieu d'avoir une méthode content sur une publication brouillon qui renvoie une chaîne de caractères vide, nous allons faire en sorte que les publications brouillons n'aient pas la méthode content du tout. Ainsi, si nous essayons d'obtenir le contenu d'une publication brouillon, nous obtiendrons une erreur de compilation nous disant que la méthode n'existe pas. En conséquence, il sera impossible pour nous d'afficher accidentellement le contenu d'une publication brouillon en production car ce code ne compilera même pas. Le Listing 17-19 montre la définition d'un struct Post et d'un struct DraftPost, ainsi que les méthodes sur chacun d'eux.

Nom de fichier : src/lib.rs

pub struct Post {
    content: String,
}

pub struct DraftPost {
    content: String,
}

impl Post {
  1 pub fn new() -> DraftPost {
        DraftPost {
            content: String::new(),
        }
    }

  2 pub fn content(&self) -> &str {
        &self.content
    }
}

impl DraftPost {
  3 pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

Listing 17-19 : Un Post avec une méthode content et un DraftPost sans méthode content

Les structs Post et DraftPost ont tous deux un champ privé content qui stocke le texte du billet de blog. Les structs n'ont plus le champ state car nous déplaçons l'encodage de l'état vers les types des structs. Le struct Post représentera une publication publiée, et il a une méthode content qui renvoie le content [2].

Nous avons toujours une fonction Post::new, mais au lieu de renvoyer une instance de Post, elle renvoie une instance de DraftPost [1]. Étant donné que content est privé et qu'il n'y a pas de fonctions qui renvoient Post, il n'est pas possible de créer une instance de Post pour le moment.

Le struct DraftPost a une méthode add_text, de sorte que nous pouvons ajouter du texte à content comme avant [3], mais notez que DraftPost n'a pas de méthode content définie! Ainsi, le programme garantit maintenant que toutes les publications commencent comme des publications brouillons, et que le contenu des publications brouillons n'est pas disponible pour l'affichage. Toute tentative de contourner ces contraintes entraînera une erreur de compilation.

Implémentation des transitions sous forme de transformations en différents types

Alors, comment obtenons-nous une publication publiée? Nous voulons appliquer la règle selon laquelle une publication brouillon doit être revue et approuvée avant d'être publiée. Une publication dans l'état en attente de révision ne devrait toujours pas afficher de contenu. Implémentons ces contraintes en ajoutant un autre struct, PendingReviewPost, en définissant la méthode request_review sur DraftPost pour renvoyer un PendingReviewPost et en définissant une méthode approve sur PendingReviewPost pour renvoyer un Post, comme montré dans le Listing 17-20.

Nom de fichier : src/lib.rs

impl DraftPost {
    --snip--
    pub fn request_review(self) -> PendingReviewPost {
        PendingReviewPost {
            content: self.content,
        }
    }
}

pub struct PendingReviewPost {
    content: String,
}

impl PendingReviewPost {
    pub fn approve(self) -> Post {
        Post {
            content: self.content,
        }
    }
}

Listing 17-20 : Un PendingReviewPost créé en appelant request_review sur DraftPost et une méthode approve qui transforme un PendingReviewPost en un Post publié

Les méthodes request_review et approve prennent la propriété de self, consommant ainsi les instances DraftPost et PendingReviewPost et les transformant respectivement en un PendingReviewPost et en un Post publié. De cette manière, nous n'aurons pas d'instances DraftPost en suspens après avoir appelé request_review sur elles, et ainsi de suite. Le struct PendingReviewPost n'a pas de méthode content définie sur lui, donc tenter de lire son contenu entraîne une erreur de compilation, comme pour DraftPost. Étant donné que le seul moyen d'obtenir une instance de Post publié qui a une méthode content définie est d'appeler la méthode approve sur un PendingReviewPost, et que le seul moyen d'obtenir un PendingReviewPost est d'appeler la méthode request_review sur un DraftPost, nous avons maintenant encodé le workflow de publication de blog dans le système de types.

Mais nous devons également apporter quelques petits changements à main. Les méthodes request_review et approve renvoient de nouvelles instances plutôt que de modifier la struct sur laquelle elles sont appelées, donc nous devons ajouter plus d'affectations de masquage let post = pour enregistrer les instances renvoyées. Nous ne pouvons également pas avoir les assertions selon lesquelles le contenu des publications brouillons et en attente de révision est une chaîne de caractères vide, et nous n'avons même pas besoin d'elles : nous ne pouvons plus compiler le code qui tente d'utiliser le contenu des publications dans ces états. Le code mis à jour de main est montré dans le Listing 17-21.

Nom de fichier : src/main.rs

use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");

    let post = post.request_review();

    let post = post.approve();

    assert_eq!("I ate a salad for lunch today", post.content());
}

Listing 17-21 : Modifications apportées à main pour utiliser la nouvelle implémentation du workflow de publication de blog

Les modifications que nous avons dû apporter à main pour réaffecter post signifient que cette implémentation ne suit plus tout à fait le patron d'état orienté objet : les transformations entre les états ne sont plus entièrement encapsulées dans l'implémentation de Post. Cependant, notre gain est que les états invalides sont désormais impossibles en raison du système de types et de la vérification de type qui se produit à la compilation! Cela garantit que certains bugs, tels que l'affichage du contenu d'une publication non publiée, seront détectés avant qu'ils ne passent en production.

Essayez les tâches suggérées au début de cette section sur le crate blog tel qu'il est après le Listing 17-21 pour voir ce que vous en pensez du design de cette version du code. Notez que certaines des tâches peuvent déjà être complétées dans ce design.

Nous avons vu que même si Rust est capable d'implémenter des patrons de conception orientés objet, d'autres patrons, tels que l'encodage de l'état dans le système de types, sont également disponibles en Rust. Ces patrons ont différents choix de compromis. Bien que vous puissiez être très familier avec les patrons orientés objet, repenser le problème pour tirer parti des fonctionnalités de Rust peut apporter des avantages, tels que la prévention de certains bugs à la compilation. Les patrons orientés objet ne seront pas toujours la meilleure solution en Rust en raison de certaines fonctionnalités, telles que la propriété, que les langages orientés objet n'ont pas.

Sommaire

Félicitations! Vous avez terminé le laboratoire sur l'implémentation d'un patron de conception orienté objet. Vous pouvez pratiquer d'autres laboratoires dans LabEx pour améliorer vos compétences.