Einführung
Willkommen zu Implementing an Object-Oriented Design Pattern. Dieser Lab ist ein Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.
In diesem Lab implementieren wir das State-Pattern in einem objektorientierten Design, um eine Blog-Beitrag-Struktur zu erstellen, die sich basierend auf ihrem Verhalten durch verschiedene Zustände (Entwurf, Überprüfung und veröffentlicht) transitions, und dabei gewährleisten, dass nur veröffentlichte Blog-Beiträge Inhalte zurückgeben können.
Implementing an Object-Oriented Design Pattern
Das State-Pattern ist ein objektorientiertes Entwurfsmuster. Der Kern des Musters ist, dass wir intern eine Menge von Zuständen definieren, die ein Wert annehmen kann. Die Zustände werden durch eine Menge von Zustandsobjekten repräsentiert, und das Verhalten des Werts ändert sich basierend auf seinem Zustand. Wir werden anhand eines Beispiels eines Blog-Beitrags-Structs arbeiten, das ein Feld hat, um seinen Zustand zu speichern, der ein Zustandsobjekt aus der Menge "Entwurf", "Überprüfung" oder "veröffentlicht" sein wird.
Die Zustandsobjekte teilen Funktionalität: Natürlich verwenden wir in Rust Structs und Traits anstelle von Objekten und Vererbung. Jedes Zustandsobjekt ist für sein eigenes Verhalten verantwortlich und dafür, zu bestimmen, wann es in einen anderen Zustand gewechselt werden sollte. Der Wert, der ein Zustandsobjekt hält, weiß nichts über das unterschiedliche Verhalten der Zustände oder wann zwischen den Zuständen gewechselt werden sollte.
Der Vorteil des Einsatzes des State-Patterns ist, dass wir, wenn sich die geschäftlichen Anforderungen des Programms ändern, nicht die Code des Werts, der den Zustand hält, oder den Code ändern müssen, der den Wert verwendet. Wir müssen nur den Code innerhalb eines der Zustandsobjekte aktualisieren, um seine Regeln zu ändern oder vielleicht weitere Zustandsobjekte hinzuzufügen.
Zuerst werden wir das State-Pattern auf eine traditionellere objektorientierte Weise implementieren, und dann werden wir einen Ansatz verwenden, der in Rust etwas natürlicher ist. Lassen Sie uns eintauchen, um mithilfe des State-Patterns einen Blog-Beitrags-Workflow schrittweise zu implementieren.
Die endgültige Funktionalität wird wie folgt aussehen:
- Ein Blog-Beitrag beginnt als leerer Entwurf.
- Wenn der Entwurf fertig ist, wird eine Überprüfung des Beitrags angefordert.
- Wenn der Beitrag genehmigt wird, wird er veröffentlicht.
- Nur veröffentlichte Blog-Beiträge geben Inhalte zurück, um gedruckt zu werden, sodass ungenehmigte Beiträge nicht versehentlich veröffentlicht werden können.
Jede andere Änderung, die an einem Beitrag versucht wird, sollte keinen Effekt haben. Beispielsweise sollte der Beitrag, wenn wir versuchen, einen Entwurf eines Blog-Beitrags zu genehmigen, bevor wir eine Überprüfung angefordert haben, weiterhin ein unveröffentlichter Entwurf bleiben.
Listing 17-11 zeigt diesen Workflow in Codeform: Dies ist ein Beispiel für die Verwendung der API, die wir in einem Bibliothekskasten namens blog implementieren werden. Dies wird noch nicht kompilieren, da wir den blog-Kasten noch nicht implementiert haben.
Dateiname: 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, der das gewünschte Verhalten zeigt, das wir von unserem blog-Kasten haben möchten
Wir möchten es dem Benutzer ermöglichen, einen neuen Entwurf eines Blog-Beitrags mit Post::new zu erstellen [1]. Wir möchten es ermöglichen, Text zum Blog-Beitrag hinzuzufügen [2]. Wenn wir versuchen, den Inhalt des Beitrags sofort zu erhalten, bevor die Genehmigung erfolgt, sollten wir keinen Text erhalten, da der Beitrag noch ein Entwurf ist. Wir haben assert_eq! im Code zu Demonstrationszwecken hinzugefügt [3]. Ein ausgezeichneter Unit-Test hierfür wäre, zu prüfen, dass ein Entwurf eines Blog-Beitrags aus der content-Methode einen leeren String zurückgibt, aber wir werden für dieses Beispiel keine Tests schreiben.
Als nächstes möchten wir eine Anfrage an die Überprüfung des Beitrags ermöglichen [4], und wir möchten, dass content einen leeren String zurückgibt, während wir auf die Überprüfung warten [5]. Wenn der Beitrag Genehmigung erhält [6], sollte er veröffentlicht werden, was bedeutet, dass der Text des Beitrags zurückgegeben wird, wenn content aufgerufen wird [7].
Beachten Sie, dass der einzige Typ, mit dem wir aus dem Kasten interagieren, der Post-Typ ist. Dieser Typ wird das State-Pattern verwenden und wird einen Wert halten, der eines von drei Zustandsobjekten sein wird, die die verschiedenen Zustände darstellen, in denen ein Beitrag sein kann - Entwurf, Überprüfung oder veröffentlicht. Das Wechseln von einem Zustand in einen anderen wird intern innerhalb des Post-Typs verwaltet. Die Zustände ändern sich als Reaktion auf die Methoden, die von den Benutzern unserer Bibliothek auf der Post-Instanz aufgerufen werden, aber sie müssen die Zustandsänderungen nicht direkt verwalten. Auch können die Benutzer keine Fehler bei den Zuständen machen, wie beispielsweise einen Beitrag veröffentlichen, bevor er überprüft wurde.
Defining Post and Creating a New Instance in the Draft State
Lassen Sie uns mit der Implementierung der Bibliothek beginnen! Wir wissen, dass wir eine öffentliche Post-Struktur benötigen, die etwas Inhalt enthält, daher werden wir mit der Definition der Struktur und einer zugehörigen öffentlichen new-Funktion beginnen, um eine Instanz von Post zu erstellen, wie in Listing 17-12 gezeigt. Wir werden auch ein privates State-Trait definieren, das das Verhalten definieren wird, das alle Zustandsobjekte für einen Post haben müssen.
Dann wird Post ein Trait-Objekt von Box<dyn State> in einem privaten Feld namens state innerhalb einer Option<T> enthalten, um das Zustandsobjekt zu halten. Sie werden gleich sehen, warum die Option<T> notwendig ist.
Dateiname: 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: Definition einer Post-Struktur und einer new-Funktion, die eine neue Post-Instanz erstellt, eines State-Traits und einer Draft-Struktur
Das State-Trait definiert das von verschiedenen Post-Zuständen geteilte Verhalten. Die Zustandsobjekte sind Draft, PendingReview und Published, und alle werden das State-Trait implementieren. Momentan hat das Trait keine Methoden, und wir werden beginnen, nur den Draft-Zustand zu definieren, da das der Zustand ist, in dem wir einen Beitrag beginnen möchten.
Wenn wir eine neue Post-Instanz erstellen, legen wir ihr state-Feld auf einen Some-Wert fest, der eine Box enthält [1]. Diese Box verweist auf eine neue Instanz der Draft-Struktur. Dies gewährleistet, dass jedes Mal, wenn wir eine neue Instanz von Post erstellen, sie als Entwurf beginnen wird. Da das state-Feld von Post privat ist, gibt es keine Möglichkeit, eine Post-Instanz in einem anderen Zustand zu erstellen! In der Post::new-Funktion legen wir das content-Feld auf einen neuen, leeren String fest [2].
Storing the Text of the Post Content
Wir haben in Listing 17-11 gesehen, dass wir eine Methode namens add_text aufrufen können und ihr einen &str übergeben, der dann als Textinhalt des Blog-Beitrags hinzugefügt wird. Wir implementieren dies als Methode, anstatt das content-Feld als pub zu exponieren, damit wir später eine Methode implementieren können, die steuert, wie die Daten des content-Felds gelesen werden. Die add_text-Methode ist ziemlich einfach, daher fügen wir in Listing 17-13 die Implementierung zum impl Post-Block hinzu.
Dateiname: src/lib.rs
impl Post {
--snip--
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
Listing 17-13: Implementierung der add_text-Methode, um Text zum content eines Beitrags hinzuzufügen
Die add_text-Methode nimmt eine mutable Referenz auf self, weil wir die Post-Instanz ändern, auf der wir add_text aufrufen. Anschließend rufen wir push_str auf der String in content auf und übergeben das text-Argument, um es zum gespeicherten content hinzuzufügen. Dieses Verhalten hängt nicht vom Zustand des Beitrags ab, daher ist es kein Teil des State-Patterns. Die add_text-Methode interagiert überhaupt nicht mit dem state-Feld, gehört aber zu dem Verhalten, das wir unterstützen möchten.
Ensuring the Content of a Draft Post Is Empty
Auch nachdem wir add_text aufgerufen und etwas Inhalt zu unserem Beitrag hinzugefügt haben, möchten wir, dass die content-Methode einen leeren String-Slice zurückgibt, da der Beitrag immer noch im Entwurfszustand ist, wie in [3] in Listing 17-11 gezeigt. Momentan implementieren wir die content-Methode mit dem einfachsten Ding, das diese Anforderung erfüllt: indem wir immer einen leeren String-Slice zurückgeben. Wir werden dies später ändern, wenn wir die Möglichkeit implementieren, einen Beitragsstatus zu ändern, sodass er veröffentlicht werden kann. Bisher können Beiträge nur im Entwurfszustand sein, daher sollte der Beitragsinhalt immer leer sein. Listing 17-14 zeigt diese Platzhalter-Implementierung.
Dateiname: src/lib.rs
impl Post {
--snip--
pub fn content(&self) -> &str {
""
}
}
Listing 17-14: Hinzufügen einer Platzhalter-Implementierung für die content-Methode auf Post, die immer einen leeren String-Slice zurückgibt
Mit dieser hinzugefügten content-Methode funktioniert alles in Listing 17-11 bis zur Zeile bei [3] wie gewünscht.
Requesting a Review Changes the Post's State
Als nächstes müssen wir Funktionalität hinzufügen, um eine Überprüfung eines Beitrags anzufordern, was seinen Zustand von Draft in PendingReview ändern sollte. Listing 17-15 zeigt diesen Code.
Dateiname: 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: Implementierung von request_review-Methoden auf Post und dem State-Trait
Wir geben Post eine öffentliche Methode namens request_review, die eine mutable Referenz auf self nimmt [1]. Dann rufen wir eine interne request_review-Methode auf dem aktuellen Zustand von Post auf [3], und diese zweite request_review-Methode konsumiert den aktuellen Zustand und gibt einen neuen Zustand zurück.
Wir fügen die request_review-Methode zum State-Trait hinzu [4]; alle Typen, die das Trait implementieren, müssen jetzt die request_review-Methode implementieren. Beachten Sie, dass anstatt self, &self oder &mut self als ersten Parameter der Methode wir self: Box<Self> haben. Diese Syntax bedeutet, dass die Methode nur gültig ist, wenn sie auf einer Box aufgerufen wird, die den Typ hält. Diese Syntax übernimmt die Eigentumsgewalt von Box<Self>, was den alten Zustand ungültig macht, sodass der Zustandswert von Post in einen neuen Zustand transformiert werden kann.
Um den alten Zustand zu konsumieren, muss die request_review-Methode die Eigentumsgewalt über den Zustandswert übernehmen. Hier kommt das Option im state-Feld von Post ins Spiel: wir rufen die take-Methode auf, um den Some-Wert aus dem state-Feld zu nehmen und an seiner Stelle ein None zu hinterlassen, weil Rust uns nicht erlaubt, unbesetzte Felder in Structs zu haben [2]. Dies ermöglicht es uns, den state-Wert aus Post zu bewegen, anstatt ihn zu entleihen. Dann werden wir den Zustandswert von Post auf das Ergebnis dieser Operation setzen.
Wir müssen state vorübergehend auf None setzen, anstatt es direkt mit Code wie self.state = self.state.request_review(); zu setzen, um die Eigentumsgewalt über den state-Wert zu erhalten. Dies gewährleistet, dass Post den alten state-Wert nicht mehr verwenden kann, nachdem wir ihn in einen neuen Zustand transformiert haben.
Die request_review-Methode auf Draft gibt eine neue, in eine Box gepackte Instanz eines neuen PendingReview-Structs zurück [5], der den Zustand repräsentiert, wenn ein Beitrag auf eine Überprüfung wartet. Die PendingReview-Struktur implementiert auch die request_review-Methode, macht aber keine Transformationen. Stattdessen gibt sie sich selbst zurück [6], weil wenn wir eine Überprüfung für einen Beitrag in einem bereits im PendingReview-Zustand anfordern, er im PendingReview-Zustand bleiben sollte.
Jetzt können wir die Vorteile des State-Patterns beginnen zu sehen: Die request_review-Methode auf Post ist die gleiche, unabhängig von ihrem state-Wert. Jeder Zustand ist für seine eigenen Regeln verantwortlich.
Wir lassen die content-Methode auf Post so wie sie ist, und geben einen leeren String-Slice zurück. Wir können jetzt einen Post sowohl im PendingReview-Zustand als auch im Draft-Zustand haben, aber wir wollen das gleiche Verhalten im PendingReview-Zustand. Listing 17-11 funktioniert jetzt bis zur Zeile bei [5]!
Adding approve to Change the Behavior of content
Die approve-Methode wird ähnlich zur request_review-Methode sein: Sie wird state auf den Wert setzen, den der aktuelle Zustand angibt, wenn dieser Zustand genehmigt wird, wie in Listing 17-16 gezeigt.
Dateiname: 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: Implementierung der approve-Methode auf Post und dem State-Trait
Wir fügen die approve-Methode zum State-Trait hinzu und erstellen eine neue Struktur, die State implementiert, den Published-Zustand.
Ähnlich wie die request_review-Methode auf PendingReview funktioniert, hat der Aufruf der approve-Methode auf einem Draft keine Auswirkungen, da approve self zurückgibt [1]. Wenn wir approve auf PendingReview aufrufen, wird eine neue, in eine Box gepackte Instanz des Published-Structs zurückgegeben [2]. Die Published-Struktur implementiert das State-Trait, und für beide Methoden request_review und approve gibt sie sich selbst zurück, da der Beitrag in diesen Fällen im Published-Zustand bleiben sollte.
Jetzt müssen wir die content-Methode auf Post aktualisieren. Wir möchten, dass der von content zurückgegebene Wert von dem aktuellen Zustand von Post abhängt, daher wird Post an eine content-Methode delegieren, die auf seinem state definiert ist, wie in Listing 17-17 gezeigt.
Dateiname: src/lib.rs
impl Post {
--snip--
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
--snip--
}
Listing 17-17: Aktualisierung der content-Methode auf Post, um an eine content-Methode auf State zu delegieren
Da das Ziel darin besteht, alle diese Regeln in den Structs zu halten, die State implementieren, rufen wir eine content-Methode auf dem Wert in state auf und übergeben die Beitragsinstanz (d.h. self) als Argument. Dann geben wir den Wert zurück, der von der Verwendung der content-Methode auf dem state-Wert zurückgegeben wird.
Wir rufen die as_ref-Methode auf der Option auf, weil wir einen Verweis auf den Wert innerhalb der Option möchten, anstatt die Eigentumsgewalt über den Wert zu erwerben. Da state eine Option<Box<dyn State>> ist, wird bei Aufruf von as_ref eine Option<&Box<dyn State>> zurückgegeben. Wenn wir as_ref nicht aufrufen würden, würden wir einen Fehler erhalten, da wir state nicht aus der entlehnten &self des Funktionsparameters heraus bewegen können.
Anschließend rufen wir die unwrap-Methode auf, von der wir wissen, dass sie niemals abstürzt, da wir wissen, dass die Methoden auf Post gewährleisten, dass state immer einen Some-Wert enthalten wird, wenn diese Methoden abgeschlossen sind. Dies ist einer der Fälle, über die wir in "Fälle, in denen Sie mehr Informationen haben als der Compiler" gesprochen haben, wenn wir wissen, dass ein None-Wert niemals möglich ist, auch wenn der Compiler dies nicht verstehen kann.
An diesem Punkt, wenn wir content auf der &Box<dyn State> aufrufen, wird die Deref-Zwangskonvertierung auf dem & und der Box wirken, sodass die content-Methode letztendlich auf dem Typ aufgerufen wird, der das State-Trait implementiert. Das bedeutet, dass wir content zur State-Trait-Definition hinzufügen müssen, und genau dort werden wir die Logik für den Inhalt ablegen, der zurückgegeben werden soll, je nachdem, welchen Zustand wir haben, wie in Listing 17-18 gezeigt.
Dateiname: 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: Hinzufügen der content-Methode zum State-Trait
Wir fügen eine Standardimplementierung für die content-Methode hinzu, die einen leeren String-Slice zurückgibt [1]. Das bedeutet, dass wir content auf den Structs Draft und PendingReview nicht implementieren müssen. Die Published-Struktur wird die content-Methode überschreiben und den Wert in post.content zurückgeben [2].
Beachten Sie, dass wir für diese Methode Lebenszeit-Anmerkungen benötigen, wie wir in Kapitel 10 diskutiert haben. Wir nehmen einen Verweis auf einen post als Argument und geben einen Verweis auf einen Teil dieses post zurück, daher ist die Lebenszeit des zurückgegebenen Verweises mit der Lebenszeit des post-Arguments verbunden.
Und fertig ist es - alles in Listing 17-11 funktioniert jetzt! Wir haben das State-Pattern mit den Regeln des Blog-Beitrags-Workflows implementiert. Die Logik, die mit den Regeln zusammenhängt, befindet sich in den Zustandsobjekten, anstatt durch Post verteilt zu sein.
Warum kein Enum?
Sie haben sich vielleicht gefragt, warum wir nicht ein
enummit den verschiedenen möglichen Beitragszuständen als Varianten verwendet haben. Das ist sicherlich eine mögliche Lösung; versuchen Sie es und vergleichen Sie die Endresultate, um zu sehen, was Ihnen besser gefällt! Ein Nachteil der Verwendung eines Enums ist, dass an jeder Stelle, an der der Wert des Enums überprüft wird, einmatch-Ausdruck oder ähnliches erforderlich ist, um jede mögliche Variante zu behandeln. Dies kann repetitiver werden als diese Trait-Objekt-Lösung.
Trade-offs of the State Pattern
Wir haben gezeigt, dass Rust in der Lage ist, das objektorientierte State-Pattern zu implementieren, um die verschiedenen Arten von Verhalten zu kapseln, die ein Beitrag in jedem Zustand haben sollte. Die Methoden auf Post wissen nichts über die verschiedenen Verhaltensweisen. Mit der Art, wie wir den Code organisiert haben, müssen wir an nur einem Ort schauen, um zu wissen, auf welche verschiedenen Arten ein veröffentlichter Beitrag verhalten kann: die Implementierung des State-Traits auf der Published-Struktur.
Wenn wir eine alternative Implementierung erstellen würden, die das State-Pattern nicht verwenden würde, könnten wir stattdessen match-Ausdrücke in den Methoden auf Post oder sogar im main-Code verwenden, der den Zustand des Beitrags überprüft und das Verhalten an diesen Stellen ändert. Das würde bedeuten, dass wir an mehreren Stellen schauen müssten, um alle Auswirkungen eines Beitrags im veröffentlichten Zustand zu verstehen! Dies würde sich nur noch erhöhen, wenn wir mehr Zustände hinzufügen würden: jeder dieser match-Ausdrücke würde einen weiteren Fall benötigen.
Mit dem State-Pattern brauchen die Post-Methoden und die Stellen, an denen wir Post verwenden, keine match-Ausdrücke, und um einen neuen Zustand hinzuzufügen, müssten wir nur eine neue Struktur hinzufügen und die Trait-Methoden auf dieser einen Struktur implementieren.
Die Implementierung mit dem State-Pattern lässt sich leicht erweitern, um weitere Funktionalität hinzuzufügen. Um die Einfachheit der Wartung von Code zu sehen, der das State-Pattern verwendet, versuchen Sie einige dieser Vorschläge:
- Fügen Sie eine
reject-Methode hinzu, die den Zustand des Beitrags vonPendingReviewzurück inDraftändert. - Erfordern Sie zwei Aufrufe von
approve, bevor der Zustand inPublishedgeändert werden kann. - Erlauben Sie es Benutzern, nur wenn ein Beitrag im
Draft-Zustand ist, Textinhalt hinzuzufügen. Tipp: Lassen Sie das Zustandsobjekt für das verantwortlich sein, was sich am Inhalt ändern könnte, aber nicht für die Modifikation vonPost.
Ein Nachteil des State-Patterns ist, dass, da die Zustände die Übergänge zwischen den Zuständen implementieren, einige der Zustände miteinander gekoppelt sind. Wenn wir einen weiteren Zustand zwischen PendingReview und Published hinzufügen, wie Scheduled, müssten wir den Code in PendingReview ändern, um zu Scheduled zu übergehen. Es wäre weniger Arbeit, wenn PendingReview nicht mit der Hinzufügung eines neuen Zustands geändert werden müsste, aber das würde bedeuten, dass wir zu einem anderen Entwurfsmuster wechseln müssten.
Ein weiterer Nachteil ist, dass wir einige Logik dupliziert haben. Um einige der Duplikation zu eliminieren, könnten wir versuchen, Standardimplementierungen für die request_review- und approve-Methoden auf dem State-Trait zu erstellen, die self zurückgeben. Dies würde jedoch nicht funktionieren: Wenn State als Trait-Objekt verwendet wird, weiß der Trait nicht genau, was das konkrete self sein wird, daher ist der Rückgabetyp zur Compile-Zeit nicht bekannt.
Andere Duplikationen umfassen die ähnlichen Implementierungen der request_review- und approve-Methoden auf Post. Beide Methoden delegieren an die Implementierung der gleichen Methode auf dem Wert im state-Feld von Option und setzen den neuen Wert des state-Felds auf das Ergebnis. Wenn wir viele Methoden auf Post hätten, die diesem Muster folgten, könnten wir überlegen, eine Makro zu definieren, um die Wiederholung zu eliminieren (siehe "Makros").
Indem wir das State-Pattern genau so implementieren, wie es für objektorientierte Sprachen definiert ist, nutzen wir die Stärken von Rust nicht so optimal wie möglich. Schauen wir uns einige Änderungen an, die wir am blog-Kratzen vornehmen können, um ungültige Zustände und Übergänge zu Kompilierungsfehlern zu machen.
Encoding States and Behavior as Types
Wir werden Ihnen zeigen, wie Sie das State-Pattern neu denken, um eine andere Reihe von Vor- und Nachteilen zu erhalten. Anstatt die Zustände und Übergänge vollständig zu kapseln, sodass der externe Code nichts von ihnen weiß, werden wir die Zustände in verschiedene Typen kodieren. Folglich wird das Typprüfsystem von Rust Compilerfehler ausgeben, um Versuche zu verhindern, Entwurfsbeiträge zu verwenden, wo nur veröffentlichte Beiträge erlaubt sind.
Betrachten wir den ersten Teil von main in Listing 17-11:
Dateiname: src/main.rs
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
}
Wir ermöglichen immer noch die Erstellung neuer Beiträge im Entwurfszustand mithilfe von Post::new und die Möglichkeit, Text zum Inhalt des Beitrags hinzuzufügen. Anstatt jedoch eine content-Methode auf einem Entwurfszustand zu haben, die einen leeren String zurückgibt, werden wir es so gestalten, dass Entwurfszustände überhaupt keine content-Methode haben. Auf diese Weise erhalten wir einen Compilerfehler, wenn wir versuchen, den Inhalt eines Entwurfszustands zu erhalten, der uns mitteilt, dass die Methode nicht existiert. Folglich wird es uns unmöglich sein, versehentlich den Inhalt eines Entwurfszustands in der Produktion anzuzeigen, da dieser Code nicht einmal kompilieren wird. Listing 17-19 zeigt die Definition einer Post-Struktur und einer DraftPost-Struktur sowie die Methoden auf jeder.
Dateiname: 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: Eine Post-Struktur mit einer content-Methode und eine DraftPost-Struktur ohne eine content-Methode
Sowohl die Post- als auch die DraftPost-Strukturen haben ein privates content-Feld, das den Blogbeitragstext speichert. Die Strukturen haben kein state-Feld mehr, da wir die Kodierung des Zustands in die Typen der Strukturen verschieben. Die Post-Struktur wird einen veröffentlichten Beitrag repräsentieren, und sie hat eine content-Methode, die den content zurückgibt [2].
Wir haben immer noch eine Post::new-Funktion, aber anstatt eine Instanz von Post zurückzugeben, gibt sie eine Instanz von DraftPost zurück [1]. Da content privat ist und es keine Funktionen gibt, die Post zurückgeben, ist es momentan nicht möglich, eine Instanz von Post zu erstellen.
Die DraftPost-Struktur hat eine add_text-Methode, sodass wir wie zuvor Text zu content hinzufügen können [3], beachten Sie jedoch, dass DraftPost keine definierte content-Methode hat! Somit stellt das Programm sicher, dass alle Beiträge als Entwurfszustände beginnen und dass der Inhalt von Entwurfszuständen nicht für die Anzeige verfügbar ist. Jeder Versuch, diesen Beschränkungen zu entgehen, führt zu einem Compilerfehler.
Implementing Transitions as Transformations into Different Types
Wie bekommen wir dann einen veröffentlichten Beitrag? Wir möchten die Regel durchsetzen, dass ein Entwurfszustand eines Beitrags überprüft und genehmigt werden muss, bevor er veröffentlicht werden kann. Ein Beitrag im Zustand „Wartet auf Überprüfung“ sollte immer noch keinen Inhalt anzeigen. Lassen Sie uns diese Beschränkungen implementieren, indem wir eine weitere Struktur, PendingReviewPost, hinzufügen, die request_review-Methode auf DraftPost definieren, um eine PendingReviewPost zurückzugeben, und eine approve-Methode auf PendingReviewPost definieren, um eine Post zurückzugeben, wie in Listing 17-20 gezeigt.
Dateiname: 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: Eine PendingReviewPost, die durch Aufruf von request_review auf DraftPost erstellt wird, und eine approve-Methode, die eine PendingReviewPost in einen veröffentlichten Post umwandelt
Die request_review- und approve-Methoden übernehmen die Eigentumsgewalt von self, verbrauchen somit die DraftPost- und PendingReviewPost-Instanzen und wandeln sie jeweils in eine PendingReviewPost und einen veröffentlichten Post um. Auf diese Weise haben wir keine verbleibenden DraftPost-Instanzen mehr, nachdem wir request_review auf ihnen aufgerufen haben, und so weiter. Die PendingReviewPost-Struktur hat keine darauf definierte content-Methode, sodass das Versuchen, ihren Inhalt zu lesen, zu einem Compilerfehler führt, wie bei DraftPost. Da der einzige Weg, um eine veröffentlichte Post-Instanz zu erhalten, die eine definierte content-Methode hat, darin besteht, die approve-Methode auf einer PendingReviewPost aufzurufen, und der einzige Weg, um eine PendingReviewPost zu erhalten, darin besteht, die request_review-Methode auf einer DraftPost aufzurufen, haben wir jetzt den Blogbeitrags-Workflow in das Typsystem kodiert.
Wir müssen aber auch einige kleine Änderungen an main vornehmen. Die request_review- und approve-Methoden geben neue Instanzen zurück, anstatt die Struktur, auf der sie aufgerufen werden, zu modifizieren, sodass wir mehr let post =-Shadowing-Zuweisungen hinzufügen müssen, um die zurückgegebenen Instanzen zu speichern. Wir können auch keine Assertionen mehr haben, dass der Inhalt der Entwurfszustände und der Zustände, die auf Überprüfung warten, leere Strings sind, und wir brauchen sie auch nicht: Wir können den Code nicht mehr kompilieren, der versucht, den Inhalt von Beiträgen in diesen Zuständen zu verwenden. Der aktualisierte Code in main ist in Listing 17-21 gezeigt.
Dateiname: 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: Änderungen an main, um die neue Implementierung des Blogbeitrags-Workflows zu verwenden
Die Änderungen, die wir an main vornehmen mussten, um post neu zuzuweisen, bedeuten, dass diese Implementierung nicht mehr ganz dem objektorientierten State-Pattern folgt: Die Transformationen zwischen den Zuständen werden nicht mehr vollständig innerhalb der Post-Implementierung kapselt. Unser Gewinn ist jedoch, dass ungültige Zustände jetzt aufgrund des Typsystems und der Typprüfsung, die zur Compile-Zeit erfolgt, unmöglich sind! Dies gewährleistet, dass bestimmte Fehler, wie die Anzeige des Inhalts eines nicht veröffentlichten Beitrags, vor der Produktion entdeckt werden.
Versuchen Sie die am Anfang dieses Abschnitts vorgeschlagenen Aufgaben auf dem blog-Kratzen, wie er nach Listing 17-21 ist, um zu sehen, was Sie über das Design dieser Version des Codes denken. Beachten Sie, dass einige der Aufgaben in diesem Design möglicherweise bereits abgeschlossen sind.
Wir haben gesehen, dass auch wenn Rust in der Lage ist, objektorientierte Entwurfsmuster zu implementieren, auch andere Muster, wie die Kodierung des Zustands in das Typsystem, in Rust verfügbar sind. Diese Muster haben unterschiedliche Vor- und Nachteile. Auch wenn Sie möglicherweise sehr vertraut mit objektorientierten Mustern sind, kann das Nachdenken über das Problem, um die Funktionen von Rust zu nutzen, Vorteile bringen, wie das Verhindern von bestimmten Fehlern zur Compile-Zeit. Objektorientierte Muster werden aufgrund bestimmter Eigenschaften, wie der Eigentumsverwaltung, die objektorientierten Sprachen nicht haben, nicht immer die beste Lösung in Rust sein.
Zusammenfassung
Herzlichen Glückwunsch! Sie haben das Lab "Implementing an Object-Oriented Design Pattern" abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.