Rust 面向?qū)ο笤O(shè)計(jì)模式的實(shí)現(xiàn)

2023-03-22 15:15 更新
ch17-03-oo-design-patterns.md
commit 851449061b74d8b15adca936350a3fca6160ff39

狀態(tài)模式state pattern)是一個面向?qū)ο笤O(shè)計(jì)模式。該模式的關(guān)鍵在于一個值有某些內(nèi)部狀態(tài),體現(xiàn)為一系列的 狀態(tài)對象,同時值的行為隨著其內(nèi)部狀態(tài)而改變。狀態(tài)對象共享功能:當(dāng)然,在 Rust 中使用結(jié)構(gòu)體和 trait 而不是對象和繼承。每一個狀態(tài)對象負(fù)責(zé)其自身的行為,以及該狀態(tài)何時應(yīng)當(dāng)轉(zhuǎn)移至另一個狀態(tài)。持有一個狀態(tài)對象的值對于不同狀態(tài)的行為以及何時狀態(tài)轉(zhuǎn)移毫不知情。

使用狀態(tài)模式意味著當(dāng)程序的業(yè)務(wù)需求改變時,無需改變值持有狀態(tài)或者使用值的代碼。我們只需更新某個狀態(tài)對象中的代碼來改變其規(guī)則,或者是增加更多的狀態(tài)對象。讓我們看看一個有關(guān)狀態(tài)模式和如何在 Rust 中使用它的例子。

為了探索這個概念,我們將實(shí)現(xiàn)一個增量式的發(fā)布博文的工作流。這個博客的最終功能看起來像這樣:

  1. 博文從空白的草案開始。
  2. 一旦草案完成,請求審核博文。
  3. 一旦博文過審,它將被發(fā)表。
  4. 只有被發(fā)表的博文的內(nèi)容會被打印,這樣就不會意外打印出沒有被審核的博文的文本。

任何其他對博文的修改嘗試都是沒有作用的。例如,如果嘗試在請求審核之前通過一個草案博文,博文應(yīng)該保持未發(fā)布的狀態(tài)。

示例 17-11 展示這個工作流的代碼形式:這是一個我們將要在一個叫做 blog 的庫 crate 中實(shí)現(xiàn)的 API 的示例。這段代碼還不能編譯,因?yàn)檫€未實(shí)現(xiàn) blog

文件名: src/main.rs

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());
}

示例 17-11: 展示了 blog crate 期望行為的代碼

我們希望允許用戶使用 Post::new 創(chuàng)建一個新的博文草案。也希望能在草案階段為博文編寫一些文本。如果在審批之前嘗試立刻獲取博文的內(nèi)容,不應(yīng)該獲取到任何文本因?yàn)椴┪娜匀皇遣莅?。一個好的單元測試將是斷言草案博文的 content 方法返回空字符串,不過我們并不準(zhǔn)備為這個例子編寫單元測試。

接下來,我們希望能夠請求審核博文,而在等待審核的階段 content 應(yīng)該仍然返回空字符串。最后當(dāng)博文審核通過,它應(yīng)該被發(fā)表,這意味著當(dāng)調(diào)用 content 時博文的文本將被返回。

注意我們與 crate 交互的唯一的類型是 Post。這個類型會使用狀態(tài)模式并會存放處于三種博文所可能的狀態(tài)之一的值 —— 草案,等待審核和發(fā)布。狀態(tài)上的改變由 Post 類型內(nèi)部進(jìn)行管理。狀態(tài)依庫用戶對 Post 實(shí)例調(diào)用的方法而改變,但是不能直接管理狀態(tài)變化。這也意味著用戶不會在狀態(tài)上犯錯,比如在過審前發(fā)布博文。

定義 Post 并新建一個草案狀態(tài)的實(shí)例

讓我們開始實(shí)現(xiàn)這個庫吧!我們知道需要一個公有 Post 結(jié)構(gòu)體來存放一些文本,所以讓我們從結(jié)構(gòu)體的定義和一個創(chuàng)建 Post 實(shí)例的公有關(guān)聯(lián)函數(shù) new 開始,如示例 17-12 所示。還需定義一個私有 trait StatePost 將在私有字段 state 中存放一個 Option<T> 類型的 trait 對象 Box<dyn State>。稍后將會看到為何 Option<T> 是必須的。

文件名: src/lib.rs

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

示例 17-12: Post 結(jié)構(gòu)體的定義和新建 Post 實(shí)例的 new 函數(shù),State trait 和結(jié)構(gòu)體 Draft

State trait 定義了所有不同狀態(tài)的博文所共享的行為,同時 Draft、PendingReview 和 Published 狀態(tài)都會實(shí)現(xiàn) State 狀態(tài)?,F(xiàn)在這個 trait 并沒有任何方法,同時開始將只定義 Draft 狀態(tài)因?yàn)檫@是我們希望博文的初始狀態(tài)。

當(dāng)創(chuàng)建新的 Post 時,我們將其 state 字段設(shè)置為一個存放了 Box 的 Some 值。這個 Box 指向一個 Draft 結(jié)構(gòu)體新實(shí)例。這確保了無論何時新建一個 Post 實(shí)例,它都會從草案開始。因?yàn)?nbsp;Post 的 state 字段是私有的,也就無法創(chuàng)建任何其他狀態(tài)的 Post 了!。Post::new 函數(shù)中將 content 設(shè)置為新建的空 String。

存放博文內(nèi)容的文本

在示例 17-11 中,展示了我們希望能夠調(diào)用一個叫做 add_text 的方法并向其傳遞一個 &str 來將文本增加到博文的內(nèi)容中。選擇實(shí)現(xiàn)為一個方法而不是將 content 字段暴露為 pub 。這意味著之后可以實(shí)現(xiàn)一個方法來控制 content 字段如何被讀取。add_text 方法是非常直觀的,讓我們在示例 17-13 的 impl Post 塊中增加一個實(shí)現(xiàn):

文件名: src/lib.rs

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

示例 17-13: 實(shí)現(xiàn)方法 add_text 來向博文的 content 增加文本

add_text 獲取一個 self 的可變引用,因?yàn)樾枰淖冋{(diào)用 add_text 的 Post 實(shí)例。接著調(diào)用 content 中的 String 的 push_str 并傳遞 text 參數(shù)來保存到 content 中。這不是狀態(tài)模式的一部分,因?yàn)樗男袨椴⒉灰蕾嚥┪乃幍臓顟B(tài)。add_text 方法完全不與 state 狀態(tài)交互,不過這是我們希望支持的行為的一部分。

確保博文草案的內(nèi)容是空的

即使調(diào)用 add_text 并向博文增加一些內(nèi)容之后,我們?nèi)匀幌M?nbsp;content 方法返回一個空字符串 slice,因?yàn)椴┪娜匀惶幱诓莅笭顟B(tài),如示例 17-11 的第 8 行所示?,F(xiàn)在讓我們使用能滿足要求的最簡單的方式來實(shí)現(xiàn) content 方法:總是返回一個空字符串 slice。當(dāng)實(shí)現(xiàn)了將博文狀態(tài)改為發(fā)布的能力之后將改變這一做法。但是目前博文只能是草案狀態(tài),這意味著其內(nèi)容應(yīng)該總是空的。示例 17-14 展示了這個占位符實(shí)現(xiàn):

文件名: src/lib.rs

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

列表 17-14: 增加一個 Post 的 content 方法的占位實(shí)現(xiàn),它總是返回一個空字符串 slice

通過增加這個 content 方法,示例 17-11 中直到第 8 行的代碼能如期運(yùn)行。

請求審核博文來改變其狀態(tài)

接下來需要增加請求審核博文的功能,這應(yīng)當(dāng)將其狀態(tài)由 Draft 改為 PendingReview。示例 17-15 展示了這個代碼:

文件名: src/lib.rs

impl Post {
    // --snip--
    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
    }
}

示例 17-15: 實(shí)現(xiàn) Post 和 State trait 的 request_review 方法

這里為 Post 增加一個獲取 self 可變引用的公有方法 request_review。接著在 Post 的當(dāng)前狀態(tài)下調(diào)用內(nèi)部的 request_review 方法,并且第二個 request_review 方法會消費(fèi)當(dāng)前的狀態(tài)并返回一個新狀態(tài)。

這里給 State trait 增加了 request_review 方法;所有實(shí)現(xiàn)了這個 trait 的類型現(xiàn)在都需要實(shí)現(xiàn) request_review 方法。注意不同于使用 self、 &self 或者 &mut self 作為方法的第一個參數(shù),這里使用了 self: Box<Self>。這個語法意味著該方法只可在持有這個類型的 Box 上被調(diào)用。這個語法獲取了 Box<Self> 的所有權(quán)使老狀態(tài)無效化,以便 Post 的狀態(tài)值可轉(zhuǎn)換為一個新狀態(tài)。

為了消費(fèi)老狀態(tài),request_review 方法需要獲取狀態(tài)值的所有權(quán)。這就是 Post 的 state 字段中 Option 的來歷:調(diào)用 take 方法將 state 字段中的 Some 值取出并留下一個 None,因?yàn)?Rust 不允許結(jié)構(gòu)體實(shí)例中存在值為空的字段。這使得我們將 state 的值移出 Post 而不是借用它。接著我們將博文的 state 值設(shè)置為這個操作的結(jié)果。

我們需要將 state 臨時設(shè)置為 None 來獲取 state 值,即老狀態(tài)的所有權(quán),而不是使用 self.state = self.state.request_review(); 這樣的代碼直接更新狀態(tài)值。這確保了當(dāng) Post 被轉(zhuǎn)換為新狀態(tài)后不能再使用老 state 值。

Draft 的 request_review 方法需要返回一個新的,裝箱的 PendingReview 結(jié)構(gòu)體的實(shí)例,其用來代表博文處于等待審核狀態(tài)。結(jié)構(gòu)體 PendingReview 同樣也實(shí)現(xiàn)了 request_review 方法,不過它不進(jìn)行任何狀態(tài)轉(zhuǎn)換。相反它返回自身,因?yàn)楫?dāng)我們請求審核一個已經(jīng)處于 PendingReview 狀態(tài)的博文,它應(yīng)該繼續(xù)保持 PendingReview 狀態(tài)。

現(xiàn)在我們能看出狀態(tài)模式的優(yōu)勢了:無論 state 是何值,Post 的 request_review 方法都是一樣的。每個狀態(tài)只負(fù)責(zé)它自己的規(guī)則。

我們將繼續(xù)保持 Post 的 content 方法實(shí)現(xiàn)不變,返回一個空字符串 slice?,F(xiàn)在我們可以擁有 PendingReview 狀態(tài)和 Draft 狀態(tài)的 Post 了,不過我們希望在 PendingReview 狀態(tài)下 Post 也有相同的行為?,F(xiàn)在示例 17-11 中直到 10 行的代碼是可以執(zhí)行的!

增加改變 content 行為的 approve 方法

approve 方法將與 request_review 方法類似:它會將 state 設(shè)置為審核通過時應(yīng)處于的狀態(tài),如示例 17-16 所示。

文件名: 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> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
    // --snip--
    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
    }
}

示例 17-16: 為 Post 和 State trait 實(shí)現(xiàn) approve 方法

這里為 State trait 增加了 approve 方法,并新增了一個實(shí)現(xiàn)了 State 的結(jié)構(gòu)體,Published 狀態(tài)。

類似于 PendingReview 中 request_review 的工作方式,如果對 Draft 調(diào)用 approve 方法,并沒有任何效果,因?yàn)樗鼤祷?nbsp;self。當(dāng)對 PendingReview 調(diào)用 approve 時,它返回一個新的、裝箱的 Published 結(jié)構(gòu)體的實(shí)例。Published 結(jié)構(gòu)體實(shí)現(xiàn)了 State trait,同時對于 request_review 和 approve 兩方法來說,它返回自身,因?yàn)樵谶@兩種情況博文應(yīng)該保持 Published 狀態(tài)。

現(xiàn)在需要更新 Post 的 content 方法。我們希望 content 根據(jù) Post 的當(dāng)前狀態(tài)返回值,所以需要 Post 代理一個定義于 state 上的 content 方法,如實(shí)例 17-17 所示:

文件名: src/lib.rs

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

示例 17-17: 更新 Post 的 content 方法來委托調(diào)用 State 的content 方法

因?yàn)槟繕?biāo)是將所有像這樣的規(guī)則保持在實(shí)現(xiàn)了 State 的結(jié)構(gòu)體中,我們將調(diào)用 state 中的值的 content 方法并傳遞博文實(shí)例(也就是 self)作為參數(shù)。接著返回 state 值的 content 方法的返回值。

這里調(diào)用 Option 的 as_ref 方法是因?yàn)樾枰?nbsp;Option 中值的引用而不是獲取其所有權(quán)。因?yàn)?nbsp;state 是一個 Option<Box<dyn State>>,調(diào)用 as_ref 會返回一個 Option<&Box<dyn State>>。如果不調(diào)用 as_ref,將會得到一個錯誤,因?yàn)椴荒軐?nbsp;state 移動出借用的 &self 函數(shù)參數(shù)。

接著調(diào)用 unwrap 方法,這里我們知道它永遠(yuǎn)也不會 panic,因?yàn)?nbsp;Post 的所有方法都確保在他們返回時 state 會有一個 Some 值。這就是一個第十二章 “當(dāng)我們比編譯器知道更多的情況” 部分討論過的我們知道 None 是不可能的而編譯器卻不能理解的情況。

接著我們就有了一個 &Box<dyn State>,當(dāng)調(diào)用其 content 時,Deref 強(qiáng)制轉(zhuǎn)換會作用于 & 和 Box ,這樣最終會調(diào)用實(shí)現(xiàn)了 State trait 的類型的 content 方法。這意味著需要為 State trait 定義增加 content,這也是放置根據(jù)所處狀態(tài)返回什么內(nèi)容的邏輯的地方,如示例 17-18 所示:

文件名: src/lib.rs

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

// --snip--
struct Published {}

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

示例 17-18: 為 State trait 增加 content 方法

這里增加了一個 content 方法的默認(rèn)實(shí)現(xiàn)來返回一個空字符串 slice。這意味著無需為 Draft 和 PendingReview 結(jié)構(gòu)體實(shí)現(xiàn) content 了。Published 結(jié)構(gòu)體會覆蓋 content 方法并會返回 post.content 的值。

注意這個方法需要生命周期注解,如第十章所討論的。這里獲取 post 的引用作為參數(shù),并返回 post 一部分的引用,所以返回的引用的生命周期與 post 參數(shù)相關(guān)。

現(xiàn)在示例完成了 —— 現(xiàn)在示例 17-11 中所有的代碼都能工作!我們通過發(fā)布博文工作流的規(guī)則實(shí)現(xiàn)了狀態(tài)模式。圍繞這些規(guī)則的邏輯都存在于狀態(tài)對象中而不是分散在 Post 之中。

狀態(tài)模式的權(quán)衡取舍

我們展示了 Rust 是能夠?qū)崿F(xiàn)面向?qū)ο蟮臓顟B(tài)模式的,以便能根據(jù)博文所處的狀態(tài)來封裝不同類型的行為。Post 的方法并不知道這些不同類型的行為。通過這種組織代碼的方式,要找到所有已發(fā)布博文的不同行為只需查看一處代碼:Published 的 State trait 的實(shí)現(xiàn)。

如果要創(chuàng)建一個不使用狀態(tài)模式的替代實(shí)現(xiàn),則可能會在 Post 的方法中,或者甚至于在 main 代碼中用到 match 語句,來檢查博文狀態(tài)并在這里改變其行為。這意味著需要查看很多位置來理解處于發(fā)布狀態(tài)的博文的所有邏輯!這在增加更多狀態(tài)時會變得更糟:每一個 match 語句都會需要另一個分支。

對于狀態(tài)模式來說,Post 的方法和使用 Post 的位置無需 match 語句,同時增加新狀態(tài)只涉及到增加一個新 struct 和為其實(shí)現(xiàn) trait 的方法。

這個實(shí)現(xiàn)易于擴(kuò)展增加更多功能。為了體會使用此模式維護(hù)代碼的簡潔性,請嘗試如下一些建議:

  • 增加 ?reject? 方法將博文的狀態(tài)從 ?PendingReview? 變回 ?Draft?
  • 在將狀態(tài)變?yōu)?nbsp;?Published? 之前需要兩次 ?approve? 調(diào)用
  • 只允許博文處于 ?Draft? 狀態(tài)時增加文本內(nèi)容。提示:讓狀態(tài)對象負(fù)責(zé)內(nèi)容可能發(fā)生什么改變,但不負(fù)責(zé)修改 ?Post?。

狀態(tài)模式的一個缺點(diǎn)是因?yàn)闋顟B(tài)實(shí)現(xiàn)了狀態(tài)之間的轉(zhuǎn)換,一些狀態(tài)會相互聯(lián)系。如果在 PendingReview 和 Published 之間增加另一個狀態(tài),比如 Scheduled,則不得不修改 PendingReview 中的代碼來轉(zhuǎn)移到 Scheduled。如果 PendingReview 無需因?yàn)樾略龅臓顟B(tài)而改變就更好了,不過這意味著切換到另一種設(shè)計(jì)模式。

另一個缺點(diǎn)是我們會發(fā)現(xiàn)一些重復(fù)的邏輯。為了消除他們,可以嘗試為 State trait 中返回 self 的 request_review 和 approve 方法增加默認(rèn)實(shí)現(xiàn),不過這會違反對象安全性,因?yàn)?trait 不知道 self 具體是什么。我們希望能夠?qū)?nbsp;State 作為一個 trait 對象,所以需要其方法是對象安全的。

另一個重復(fù)是 Post 中 request_review 和 approve 這兩個類似的實(shí)現(xiàn)。他們都委托調(diào)用了 state 字段中 Option 值的同一方法,并在結(jié)果中為 state 字段設(shè)置了新值。如果 Post 中的很多方法都遵循這個模式,我們可能會考慮定義一個宏來消除重復(fù)(查看第十九章的 “宏” 部分)。

完全按照面向?qū)ο笳Z言的定義實(shí)現(xiàn)這個模式并沒有盡可能地利用 Rust 的優(yōu)勢。讓我們看看一些代碼中可以做出的修改,來將無效的狀態(tài)和狀態(tài)轉(zhuǎn)移變?yōu)榫幾g時錯誤。

將狀態(tài)和行為編碼為類型

我們將展示如何稍微反思狀態(tài)模式來進(jìn)行一系列不同的權(quán)衡取舍。不同于完全封裝狀態(tài)和狀態(tài)轉(zhuǎn)移使得外部代碼對其毫不知情,我們將狀態(tài)編碼進(jìn)不同的類型。如此,Rust 的類型檢查就會將任何在只能使用發(fā)布博文的地方使用草案博文的嘗試變?yōu)榫幾g時錯誤。

讓我們考慮一下示例 17-11 中 main 的第一部分:

文件名: src/main.rs

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

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

我們?nèi)匀幌M軌蚴褂?nbsp;Post::new 創(chuàng)建一個新的草案博文,并能夠增加博文的內(nèi)容。不過不同于存在一個草案博文時返回空字符串的 content 方法,我們將使草案博文完全沒有 content 方法。這樣如果嘗試獲取草案博文的內(nèi)容,將會得到一個方法不存在的編譯錯誤。這使得我們不可能在生產(chǎn)環(huán)境意外顯示出草案博文的內(nèi)容,因?yàn)檫@樣的代碼甚至就不能編譯。示例 17-19 展示了 Post 結(jié)構(gòu)體、DraftPost 結(jié)構(gòu)體以及各自的方法的定義:

文件名: src/lib.rs

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);
    }
}

示例 17-19: 帶有 content 方法的 Post 和沒有 content 方法的 DraftPost

Post 和 DraftPost 結(jié)構(gòu)體都有一個私有的 content 字段來儲存博文的文本。這些結(jié)構(gòu)體不再有 state 字段因?yàn)槲覀儗顟B(tài)編碼改為結(jié)構(gòu)體類型。Post 將代表發(fā)布的博文,它有一個返回 content 的 content 方法。

仍然有一個 Post::new 函數(shù),不過不同于返回 Post 實(shí)例,它返回 DraftPost 的實(shí)例?,F(xiàn)在不可能創(chuàng)建一個 Post 實(shí)例,因?yàn)?nbsp;content 是私有的同時沒有任何函數(shù)返回 Post。

DraftPost 上定義了一個 add_text 方法,這樣就可以像之前那樣向 content 增加文本,不過注意 DraftPost 并沒有定義 content 方法!如此現(xiàn)在程序確保了所有博文都從草案開始,同時草案博文沒有任何可供展示的內(nèi)容。任何繞過這些限制的嘗試都會產(chǎn)生編譯錯誤。

實(shí)現(xiàn)狀態(tài)轉(zhuǎn)移為不同類型的轉(zhuǎn)換

那么如何得到發(fā)布的博文呢?我們希望強(qiáng)制執(zhí)行的規(guī)則是草案博文在可以發(fā)布之前必須被審核通過。等待審核狀態(tài)的博文應(yīng)該仍然不會顯示任何內(nèi)容。讓我們通過增加另一個結(jié)構(gòu)體 PendingReviewPost 來實(shí)現(xiàn)這個限制,在 DraftPost 上定義 request_review 方法來返回 PendingReviewPost,并在 PendingReviewPost 上定義 approve 方法來返回 Post,如示例 17-20 所示:

文件名: 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,
        }
    }
}

列表 17-20: PendingReviewPost 通過調(diào)用 DraftPost 的 request_review 創(chuàng)建,approve 方法將 PendingReviewPost 變?yōu)榘l(fā)布的 Post

request_review 和 approve 方法獲取 self 的所有權(quán),因此會消費(fèi) DraftPost 和 PendingReviewPost 實(shí)例,并分別轉(zhuǎn)換為 PendingReviewPost 和發(fā)布的 Post。這樣在調(diào)用 request_review 之后就不會遺留任何 DraftPost 實(shí)例,后者同理。PendingReviewPost 并沒有定義 content 方法,所以嘗試讀取其內(nèi)容會導(dǎo)致編譯錯誤,DraftPost 同理。因?yàn)槲ㄒ坏玫蕉x了 content 方法的 Post 實(shí)例的途徑是調(diào)用 PendingReviewPost 的 approve 方法,而得到 PendingReviewPost 的唯一辦法是調(diào)用 DraftPost 的 request_review 方法,現(xiàn)在我們就將發(fā)博文的工作流編碼進(jìn)了類型系統(tǒng)。

這也意味著不得不對 main 做出一些小的修改。因?yàn)?nbsp;request_review 和 approve 返回新實(shí)例而不是修改被調(diào)用的結(jié)構(gòu)體,所以我們需要增加更多的 let post = 覆蓋賦值來保存返回的實(shí)例。也不再能斷言草案和等待審核的博文的內(nèi)容為空字符串了,我們也不再需要他們:不能編譯嘗試使用這些狀態(tài)下博文內(nèi)容的代碼。更新后的 main 的代碼如示例 17-21 所示:

文件名: 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());
}

示例 17-21: main 中使用新的博文工作流實(shí)現(xiàn)的修改

不得不修改 main 來重新賦值 post 使得這個實(shí)現(xiàn)不再完全遵守面向?qū)ο蟮臓顟B(tài)模式:狀態(tài)間的轉(zhuǎn)換不再完全封裝在 Post 實(shí)現(xiàn)中。然而,得益于類型系統(tǒng)和編譯時類型檢查,我們得到了的是無效狀態(tài)是不可能的!這確保了某些特定的 bug,比如顯示未發(fā)布博文的內(nèi)容,將在部署到生產(chǎn)環(huán)境之前被發(fā)現(xiàn)。

嘗試為示例 17-20 之后的 blog crate 實(shí)現(xiàn)這一部分開始所建議的增加額外需求的任務(wù)來體會使用這個版本的代碼是何感覺。注意在這個設(shè)計(jì)中一些需求可能已經(jīng)完成了。

即便 Rust 能夠?qū)崿F(xiàn)面向?qū)ο笤O(shè)計(jì)模式,也有其他像將狀態(tài)編碼進(jìn)類型這樣的模式存在。這些模式有著不同的權(quán)衡取舍。雖然你可能非常熟悉面向?qū)ο竽J?,重新思考這些問題來利用 Rust 提供的像在編譯時避免一些 bug 這樣有益功能。在 Rust 中面向?qū)ο竽J讲⒉豢偸亲詈玫慕鉀Q方案,因?yàn)?Rust 擁有像所有權(quán)這樣的面向?qū)ο笳Z言所沒有的功能。

總結(jié)

閱讀本章后,不管你是否認(rèn)為 Rust 是一個面向?qū)ο笳Z言,現(xiàn)在你都見識了 trait 對象是一個 Rust 中獲取部分面向?qū)ο蠊δ艿姆椒?。動態(tài)分發(fā)可以通過犧牲少量運(yùn)行時性能來為你的代碼提供一些靈活性。這些靈活性可以用來實(shí)現(xiàn)有助于代碼可維護(hù)性的面向?qū)ο竽J?。Rust 也有像所有權(quán)這樣不同于面向?qū)ο笳Z言的功能。面向?qū)ο竽J讲⒉豢偸抢?Rust 優(yōu)勢的最好方式,但也是可用的選項(xiàng)。

接下來,讓我們看看另一個提供了多樣靈活性的 Rust 功能:模式。貫穿全書的模式, 我們已經(jīng)和它們打過照面了,但并沒有見識過它們的全部本領(lǐng)。讓我們開始探索吧!


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號