Implémenter un patron de conception orienté objet
Le patron é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 état, et le comportement de la valeur change en fonction de son état. Nous allons travailler sur un exemple de struct d’article de blog qui à un champ pour contenir son état, qui sera un objet état de l’ensemble « brouillon », « en revue » ou « publié ».
Les objets état partagent des fonctionnalités : en Rust, bien sûr, nous utilisons des structs et des traits plutôt que des objets et l’héritage. Chaque objet état est responsable de son propre comportement et de déterminer quand il doit changer vers un autre état. La valeur qui contient un objet état ne sait rien du comportement différent des états ni de quand effectuer la transition entre les états.
L’avantage d’utiliser le patron état est que, quand les exigences métier du programme changent, nous n’aurons pas besoin de modifier le code de la valeur contenant l’état ni le code qui utilise la valeur. Nous n’aurons qu’à mettre à jour le code à l’intérieur d’un des objets état pour changer ses règles, ou peut-être ajouter d’autres objets état.
D’abord, nous allons implémenter le patron état de manière plus traditionnelle orientée objet. Ensuite, nous utiliserons une approche un peu plus naturelle en Rust. Plongeons-y pour implémenter progressivement un flux de travail d’articles de blog en utilisant le patron état.
La fonctionnalité finale ressemblera à ceci :
- Un article de blog commence comme un brouillon vide.
- Quand le brouillon est terminé, une revue de l’article est demandée.
- Quand l’article est approuvé, il est publié.
- Seuls les articles de blog publiés retournent du contenu à afficher pour que les articles non approuvés ne puissent pas être publiés accidentellement.
Toute autre modification tentée sur un article ne devrait avoir aucun effet. Par exemple, si nous essayons d’approuver un brouillon d’article de blog avant d’avoir demandé une revue, l’article devrait rester un brouillon non publié.
Tenter le style orienté objet traditionnel
Il existe une infinité de façons de structurer du code pour résoudre le même problème, chacune avec des compromis différents. L’implémentation de 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 certaines des forces de Rust. Plus tard, nous démontrerons une solution différente qui utilise toujours le patron de conception orienté objet mais qui est structurée d’une manière qui pourrait sembler moins familière aux programmeurs ayant de l’expérience en programmation orientée objet. Nous comparerons les deux solutions pour expérimenter les compromis de la conception de code Rust différemment du code dans d’autres langages.
L’encart 18-11 montre ce flux de travail sous forme de code : c’est un exemple d’utilisation de l’API que nous implémenterons dans un crate de bibliothèque nommé blog. Cela ne compilera pas encore car nous n’avons pas implémenté le crate blog.
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
blog crate to haveNous voulons permettre à l’utilisateur de créer un nouveau brouillon d’article de blog avec Post::new. Nous voulons permettre d’ajouter du texte à l’article de blog. Si nous essayons d’obtenir le contenu de l’article immédiatement, avant l’approbation, nous ne devrions obtenir aucun texte car l’article est encore un brouillon. Nous avons ajouté assert_eq! dans le code à des fins de démonstration. Un excellent test unitaire pour cela serait de vérifier qu’un brouillon d’article de blog retourné une chaîne vide depuis la méthode content, mais nous n’allons pas écrire de tests pour cet exemple.
Ensuite, nous voulons permettre de demander une revue de l’article, et nous voulons que content retourné une chaîne vide en attendant la revue. Quand l’article reçoit l’approbation, il devrait être publié, ce qui signifie que le texte de l’article sera retourné quand content est appelé.
Remarquez que le seul type avec lequel nous interagissons depuis le crate est le type Post. Ce type utilisera le patron état et contiendra une valeur qui sera l’un des trois objets état représentant les différents états dans lesquels un article peut se trouver — brouillon, revue ou publié. Le passage d’un état à un autre sera géré en interne au sein du 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 à gérer directement les changements d’état. De plus, les utilisateurs ne peuvent pas faire d’erreur avec les états, comme publier un article avant qu’il ne soit révisé.
Définir Post et créer une nouvelle instance
Commençons l’implémentation de la bibliothèque ! Nous savons que nous avons besoin d’une struct publique Post qui contient du contenu, donc nous commencerons par la définition de la struct et une fonction publique associée new pour créer une instance de Post, comme montré dans l’encart 18-12. Nous créerons aussi un trait privé State qui définira le comportement que tous les objets état d’un Post doivent avoir.
Ensuite, Post contiendra un objet trait de Box<dyn State> à l’intérieur d’un Option<T> dans un champ privé nommé state pour contenir l’objet état. Vous verrez bientôt pourquoi l’Option<T> est nécessaire.
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
}
trait State {}
struct Draft {}
impl State for Draft {}
Post struct and a new function that creates a new Post instance, a State trait, and a Draft structLe trait State définit le comportement partagé par les différents états d’article. Les objets état sont Draft, PendingReview et Published, et ils implémenteront tous le trait State. Pour l’instant, le trait n’à aucune méthode, et nous commencerons par définir uniquement l’état Draft car c’est l’état dans lequel nous voulons qu’un article démarre.
Quand nous créons un nouveau Post, nous définissons son champ state à une valeur Some qui contient un Box. Ce Box pointe vers une nouvelle instance de la struct Draft. Cela garantit que chaque fois que nous créons une nouvelle instance de Post, elle commencera en tant que brouillon. Comme le champ state de Post est privé, il n’y à aucun moyen de créer un Post dans un autre état ! Dans la fonction Post::new, nous définissons le champ content à une nouvelle String vide.
Stocker le texte du contenu de l’article
Nous avons vu dans l’encart 18-11 que nous voulons pouvoir appeler une méthode nommée add_text et lui passer un &str qui sera ensuite ajouté comme contenu texte de l’article de blog. Nous implémentons cela comme une méthode, plutôt que d’exposer le champ content comme pub, pour que plus tard nous puissions implémenter une méthode qui contrôlera comment les données du champ content sont lues. La méthode add_text est assez simple, alors ajoutons l’implémentation dans l’encart 18-13 au bloc impl Post.
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
trait State {}
struct Draft {}
impl State for Draft {}
add_text method to add text to a post’s contentLa méthode add_text prend une référence mutable vers self car nous modifions l’instance 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 sauvegardé. Ce comportement ne dépend pas de l’état dans lequel se trouve l’article, donc il ne fait pas partie du patron état. La méthode add_text n’interagit pas du tout avec le champ state, mais elle fait partie du comportement que nous voulons supporter.
S’assurer que le contenu d’un brouillon d’article est vide
Même après avoir appelé add_text et ajouté du contenu à notre article, nous voulons toujours que la méthode content retourné une tranche de chaîne vide car l’article est encore dans l’état brouillon, comme montré par le premier assert_eq! dans l’encart 18-11. Pour l’instant, implémentons la méthode content avec la chose la plus simple qui satisfera cette exigence : toujours retourner une tranche de chaîne vide. Nous changerons cela plus tard quand nous implémenterons la capacité de changer l’état d’un article pour qu’il puisse être publié. Jusqu’ici, les articles ne peuvent être que dans l’état brouillon, donc le contenu de l’article devrait toujours être vide. L’encart 18-14 montre cette implémentation provisoire.
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
}
trait State {}
struct Draft {}
impl State for Draft {}
content method on Post that always returns an empty string sliceAvec cette méthode content ajoutée, tout dans l’encart 18-11 jusqu’au premier assert_eq! fonctionne comme prévu.
Demander une revue, ce qui change l’état de l’article
Ensuite, nous devons ajouter la fonctionnalité pour demander une revue d’un article, ce qui devrait changer son état de Draft à PendingReview. L’encart 18-15 montre ce code.
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
}
request_review methods on Post and the State traitNous donnons à Post une méthode publique nommée request_review qui prendra une référence mutable vers self. Ensuite, nous appelons une méthode interne request_review sur l’état actuel de Post, et cette seconde méthode request_review consomme l’état actuel et retourné un nouvel état.
Nous ajoutons la méthode request_review au trait State ; tous les types qui implémentent le trait devront maintenant 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 un Box contenant le type. Cette syntaxe prend possession de Box<Self>, invalidant l’ancien état pour que la valeur d’état du Post puisse se transformer en un nouvel état.
Pour consommer l’ancien état, la méthode request_review doit prendre possession de la valeur d’état. C’est là que l’Option dans le champ state de Post entre en jeu : nous appelons la méthode take pour extraire la valeur Some du champ state et laisser un None à sa place car Rust ne nous laisse pas avoir des champs non remplis dans les structs. Cela nous permet de déplacer la valeur state hors de Post plutôt que de l’emprunter. Ensuite, nous définirons la valeur state de l’article au résultat de cette opération.
Nous devons mettre state à None temporairement plutôt que de le définir directement avec du code comme self.state = self.state.request_review(); pour obtenir la possession de la valeur state. Cela garantit que Post ne peut pas utiliser l’ancienne valeur state après que nous l’ayons transformée en un nouvel état.
La méthode request_review sur Draft retourné une nouvelle instance boxée d’une nouvelle struct PendingReview, qui représente l’état quand un article attend une revue. La struct PendingReview implémente aussi la méthode request_review mais ne fait aucune transformation. Elle se retourné elle-même car quand nous demandons une revue sur un article déjà dans l’état PendingReview, il devrait rester dans l’état PendingReview.
Nous pouvons maintenant commencer à voir les avantages du patron état : la méthode request_review sur Post est la même quel que soit sa valeur state. Chaque état est responsable de ses propres règles.
Nous laisserons la méthode content de Post telle quelle, retournant une tranche de chaîne vide. Nous pouvons maintenant avoir un Post dans l’état PendingReview aussi bien que dans l’état Draft, mais nous voulons le même comportement dans l’état PendingReview. L’encart 18-11 fonctionne maintenant jusqu’au deuxième appel assert_eq! !
Ajouter approve pour changer le comportement de content
La méthode approve sera similaire à la méthode request_review : elle définira state à la valeur que l’état actuel dit qu’il devrait avoir quand cet état est approuvé, comme montré dans l’encart 18-16.
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
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 request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
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
}
}
approve method on Post and the State traitNous ajoutons la méthode approve au trait State et ajoutons une nouvelle struct qui implémente State, l’état Published.
De la même manière que request_review fonctionne sur PendingReview, si nous appelons la méthode approve sur un Draft, cela n’aura aucun effet car approve retournera self. Quand nous appelons approve sur PendingReview, il retourné une nouvelle instance boxée de la struct Published. La struct Published implémente le trait State, et pour les méthodes request_review et approve, elle se retourné elle-même car l’article devrait rester dans l’état Published dans ces cas.
Maintenant, nous devons mettre à jour la méthode content de Post. Nous voulons que la valeur retourné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 l’encart 18-17.
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
// --snip--
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
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 {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
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
}
}
content method on Post to delegate to a content method on StateComme l’objectif est de garder toutes ces règles à l’intérieur des structs qui implémentent State, nous appelons une méthode content sur la valeur dans state et passons l’instance de l’article (c’est-à-dire self) comme argument. Ensuite, nous retournons la valeur retourné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 vers la valeur à l’intérieur de l’Option plutôt que la possession de la valeur. Comme state est un Option<Box<dyn State>>, quand nous appelons as_ref, un Option<&Box<dyn State>> est retourné. Si nous n’appelions pas as_ref, nous obtiendrions une erreur car nous ne pouvons pas déplacer state hors du &self emprunté du paramètre de la fonction.
Nous appelons ensuite la méthode unwrap, dont nous savons qu’elle ne paniquera jamais car nous savons que les méthodes de Post garantissent que state contiendra toujours une valeur Some quand ces méthodes sont terminées. C’est l’un des cas dont nous avons parlé dans la section [« Quand vous avez plus d’informations que le compilateur »][more-info-than-rustc] du chapitre 9, quand nous savons qu’une valeur None n’est jamais possible, même si le compilateur n’est pas en mesure de le comprendre.
À ce stade, quand nous appelons content sur le &Box<dyn State>, la coercition de déréférencement prendra effet sur le & et le Box pour que la méthode content soit 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 déterminer quel contenu retourner en fonction de l’état dans lequel nous sommes, comme montré dans l’encart 18-18.
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
fn content<'a>(&self, post: &'a Post) -> &'a str {
""
}
}
// --snip--
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
fn content<'a>(&self, post: &'a Post) -> &'a str {
&post.content
}
}
content method to the State traitNous ajoutons une implémentation par défaut pour la méthode content qui retourné une tranche de chaîne vide. Cela signifie que nous n’avons pas besoin d’implémenter content sur les structs Draft et PendingReview. La struct Published surchargera la méthode content et retournera la valeur dans post.content. Bien que pratique, avoir la méthode content sur State qui détermine le contenu du Post brouille les frontières entre la responsabilité de State et la responsabilité de Post.
Notez que nous avons besoin d’annotations de durée de vie sur cette méthode, comme nous l’avons discuté au chapitre 10. Nous prenons une référence vers un post comme argument et retournons une référence vers une partie de ce post, donc la durée de vie de la référence retournée est liée à la durée de vie de l’argument post.
Et c’est terminé — tout l’encart 18-11 fonctionne maintenant ! Nous avons implémenté le patron état avec les règles du flux de travail des articles de blog. La logique liée aux règles réside dans les objets état plutôt que d’être dispersée dans Post.
Pourquoi pas une énumération ?
Vous vous êtes peut-être demandé pourquoi nous n’avons pas utilisé une énumération avec les différents états possibles de l’article comme variantes. C’est certainement une solution possible ; essayez-la et comparez les résultats finaux pour voir laquelle vous préférez ! Un inconvénient de l’utilisation d’une énumération est que chaque endroit qui vérifie la valeur de l’énumération aura besoin d’une expression match ou similaire pour gérer chaque variante possible. Cela pourrait devenir plus répétitif que cette solution à base d’objets trait.
Évaluer le patron état
Nous avons montré que Rust est capable d’implémenter le patron état orienté objet pour encapsuler les différents types de comportement qu’un article devrait avoir dans chaque état. Les méthodes de Post ne savent rien des différents comportements. Grâce à la façon dont nous avons organisé le code, nous n’avons qu’un seul endroit à regarder pour connaître les différentes façons dont un article publié peut se comporter : l’implémentation du trait State sur la struct Published.
Si nous devions créer une implémentation alternative qui n’utilise pas le patron état, nous pourrions plutôt utiliser des expressions match dans les méthodes de Post ou même dans le code main qui vérifie l’état de l’article et change le comportement à ces endroits. Cela signifierait que nous devrions regarder à plusieurs endroits pour comprendre toutes les implications d’un article dans l’état publié.
Avec le patron é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, nous n’aurions qu’à ajouter une nouvelle struct et implémenter les méthodes du trait sur cette struct à un seul endroit.
L’implémentation utilisant le patron état est facile à étendre pour ajouter plus de fonctionnalités. Pour voir la simplicité de la maintenance du code qui utilise le patron état, essayez quelques-unes de ces suggestions :
- Ajouter une méthode
rejectqui change l’état de l’article dePendingReviewàDraft. - Exiger deux appels à
approveavant que l’état puisse être changé enPublished. - Permettre aux utilisateurs d’ajouter du contenu texte uniquement quand un article est dans l’état
Draft. Indice : faites en sorte que l’objet état soit responsable de ce qui pourrait changer dans le contenu mais pas de la modification duPost.
Un inconvénient du patron état est que, comme les états implémentent les transitions entre états, certains des états sont couplés les uns aux autres. Si nous ajoutons un autre état entre PendingReview et Published, comme Scheduled, nous devrions changer le code dans PendingReview pour effectuer la transition vers Scheduled à la place. Ce serait 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 partie de la 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 du trait State qui retournent self. Cependant, cela ne fonctionnerait pas : en utilisant State comme objet trait, le trait ne sait pas ce que sera exactement le self concret, donc le type de retour n’est pas connu au moment de la compilation. (C’est l’une des règles de compatibilité dyn mentionnées plus tôt.)
D’autres duplications incluent les implémentations similaires des méthodes request_review et approve de Post. Les deux méthodes utilisent Option::take avec le champ state de Post, et si state est Some, elles délèguent à l’implémentation de la même méthode de la valeur encapsulée et définissent la nouvelle valeur du champ state au résultat. Si nous avions beaucoup de méthodes sur Post qui suivaient ce motif, nous pourrions envisager de définir une macro pour éliminer la répétition (voir la section [« Macros »][macros] au chapitre 20).
En implémentant le patron état exactement comme il est défini pour les langages orientés objet, nous ne tirons pas pleinement parti des forces de Rust comme nous le pourrions. Voyons quelques changements que nous pouvons apporter au crate blog pour transformer les états et transitions invalides en erreurs de compilation.
Encoder les états et le comportement comme des types
Nous allons vous montrer comment repenser le patron état pour obtenir un ensemble différent de compromis. Plutôt que d’encapsuler complètement les états et les transitions pour que le code extérieur n’en ait aucune connaissance, nous encoderons les états dans différents types. Par conséquent, le système de vérification de types de Rust empêchera les tentatives d’utiliser des brouillons d’articles là où seuls les articles publiés sont autorisés en émettant une erreur de compilation.
Considérons la première partie de main dans l’encart 18-11 :
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
Nous permettons toujours la création de nouveaux articles dans l’état brouillon en utilisant Post::new et la capacité d’ajouter du texte au contenu de l’article. Mais au lieu d’avoir une méthode content sur un brouillon d’article qui retourné une chaîne vide, nous ferons en sorte que les brouillons d’articles n’aient pas du tout la méthode content. De cette façon, si nous essayons d’obtenir le contenu d’un brouillon d’article, nous obtiendrons une erreur du compilateur nous disant que la méthode n’existe pas. En conséquence, il sera impossible pour nous d’afficher accidentellement le contenu d’un brouillon d’article en production car ce code ne compilera même pas. L’encart 18-19 montre la définition d’une struct Post et d’une struct DraftPost, ainsi que les méthodes sur chacune.
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
Post with a content method and a DraftPost without a content methodLes structs Post et DraftPost ont toutes deux un champ privé content qui stocké le texte de l’article 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. La struct Post représentera un article publié, et elle à une méthode content qui retourné le content.
Nous avons toujours une fonction Post::new, mais au lieu de retourner une instance de Post, elle retourné une instance de DraftPost. Comme content est privé et qu’il n’y a pas de fonctions qui retournent Post, il n’est pas possible de créer une instance de Post pour l’instant.
La struct DraftPost à une méthode add_text, donc nous pouvons ajouter du texte à content comme avant, mais notez que DraftPost n’a pas de méthode content définie ! Donc maintenant le programme garantit que tous les articles commencent comme des brouillons, et les brouillons n’ont pas leur contenu disponible pour l’affichage. Toute tentative de contourner ces contraintes résultera en une erreur du compilateur.
Alors, comment obtenir un article publié ? Nous voulons imposer la règle qu’un brouillon d’article doit être révisé et approuvé avant de pouvoir être publié. Un article en état de revue en attente ne devrait toujours pas afficher de contenu. Implémentons ces contraintes en ajoutant une autre struct, PendingReviewPost, en définissant la méthode request_review sur DraftPost pour retourner un PendingReviewPost et en définissant une méthode approve sur PendingReviewPost pour retourner un Post, comme montré dans l’encart 18-20.
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
// --snip--
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
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,
}
}
}
PendingReviewPost that gets created by calling request_review on DraftPost and an approve method that turns a PendingReviewPost into a published PostLes méthodes request_review et approve prennent possession de self, consommant ainsi les instances DraftPost et PendingReviewPost et les transformant en un PendingReviewPost et un Post publié, respectivement. De cette façon, nous n’aurons pas d’instances DraftPost persistantes après avoir appelé request_review sur elles, et ainsi de suite. La struct PendingReviewPost n’a pas de méthode content définie, donc tenter de lire son contenu résulte en une erreur du compilateur, comme avec DraftPost. Comme le seul moyen d’obtenir une instance de Post publiée qui à une méthode content définie est d’appeler la méthode approve sur un PendingReviewPost, et le seul moyen d’obtenir un PendingReviewPost est d’appeler la méthode request_review sur un DraftPost, nous avons maintenant encodé le flux de travail des articles de blog dans le système de types.
Mais nous devons aussi apporter quelques petites modifications à main. Les méthodes request_review et approve retournent de nouvelles instances plutôt que de modifier la struct sur laquelle elles sont appelées, donc nous devons ajouter plus d’assignations par masquage let post = pour sauvegarder les instances retournées. Nous ne pouvons pas non plus avoir les assertions sur le contenu des brouillons et des articles en revue en attente comme des chaînes vides, et nous n’en avons pas besoin : nous ne pouvons plus compiler du code qui essaie d’utiliser le contenu des articles dans ces états. Le code mis à jour dans main est montré dans l’encart 18-21.
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());
}
main to use the new implementation of the blog post workflowLes changements que nous avons dû apporter à main pour réassigner post signifient que cette implémentation ne suit plus tout à fait le patron é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 maintenant impossibles grâce au système de types et à la vérification de types qui se fait au moment de la compilation ! Cela garantit que certains bugs, comme l’affichage du contenu d’un article non publié, seront découverts avant qu’ils n’arrivent 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 l’encart 18-21 pour voir ce que vous pensez de la conception de cette version du code. Notez que certaines des tâches pourraient déjà être complétées dans 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, comme encoder l’état dans le système de types, sont aussi disponibles en Rust. Ces patrons ont des compromis différents. 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, comme prévenir certains bugs au moment de la compilation. Les patrons orientés objet ne seront pas toujours la meilleure solution en Rust en raison de certaines 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 des objets trait pour obtenir certaines fonctionnalités orientées objet en Rust. La répartition dynamique peut donner à votre code une certaine flexibilité en échange d’un peu de performance à l’exécution. Vous pouvez utiliser cette flexibilité pour implémenter des patrons orientés objet qui peuvent aider la maintenabilité de votre code. Rust a aussi d’autres fonctionnalités, comme la possession, que les langages orientés objet n’ont pas. Un patron orienté objet ne sera pas toujours la meilleure façon de tirer parti des forces de Rust, mais c’est une option disponible.
Ensuite, nous examinerons les motifs, qui sont une autre fonctionnalité de Rust permettant beaucoup de flexibilité. Nous les avons examinés brièvement tout au long du livre mais n’avons pas encore vu toutes leurs capacités. Allons-y !