Rust 中的構造器 - 1 意義及好處

在這篇文章中,我們將討論 “構造器模式”。構建器模式是一個 API 設計模式,用於構造 Rust 結構體實例。我們將討論使用構造器的意義,使用的一些好處及應用。

例子

這有一些常用的 Rust crate 中構造器的例子:

標準庫中的 "Command":

Command::new("cmd")
    .args(["/C", "echo hello"])
    .output()

Rocket

rocket::build()
    .mount("/hello", routes![world])
    .launch()

Http crate 中的 "Response":

Response::builder()
    .status(200)
    .header("X-Custom-Foo", "Bar")
    .header("Set-Cookie", "key=2")
    .body(())
    .unwrap();

好了,讓我們來深入研究一下構造器模式到底是什麼。

構造器模式是什麼

定義以下結構體:

struct Message {
    from: String,
    content: String,
    attachment: Option<String>
}

使用結構體初始化語法:

Message {
    from: "John Smith".into(),
    content: "Hello!".into(),
    attachment: None
}

使用構造器模式初始化:

Message::builder()
    .from("John Smith".into())
    .content("Hello!".into())
    .build()

構造器組成:

構造器模式遵循函數式編程設計,類似於構造迭代器。

設置值的方法接受一個對構造器的可變引用,並返回相同的引用,便於鏈式調用。使用可變引用的便利之處在於它可以在函數和 if 語句之間共享:

fn build_message_from_console_input(
    builder: &mut MessageBuilder
) -> Result<(), Box<dyn Error>> {
    let mut buffer = String::new();
    let mut stdin = std::io::stdin();
    stdin.read_line(&mut buffer).unwrap();
    let split = buffer.rsplit_once("with attachment: ");
    if let Some((message, attachment_path)) = split {
        let attachment = 
            std::fs::read_to_string(attachment_path).unwrap();
        builder
            .content(message.into());
            .attachment(attachment);
    } else {
        builder.text_filter(buffer);
    }
}

接下來,我們將探索構造器模式提供很多好處的一些地方。

約束和封裝數據

下面定義的結構體,表示在特定的時間運行特定的函數:

struct FutureRequest<T: FnOnce()> {
    at: chrono::DateTime<chrono::Utc>,
    func: T
}

我們不想讓程序隨便給定一個時間就能創建 FutureRequest 結構體,對於常規的結構體初始化和公共字段,沒有一個好的方法來約束給定的值:

let fq = FutureRequest {
    at: chrono::DateTime::from_utc(
        chrono::NaiveDate::from_ymd(-112, 2, 18)
            .and_hms(11, 5, 6), 
        Utc
    ),
    func: || println!("𓅥𓃶𓀫"),
}

然而,通過構造器模式中設置時間的方法,我們可以在賦值之前驗證該值:

#[derive(Debug)]
struct SchedulingInPastError;
impl<T: FnOnce() -> ()> FutureRequestBuilder<T> {
    fn at(
        &mut self, 
        date_time: chrono::DateTime<Utc>
    ) -> Result<&mut Self, SchedulingInPastError> {
        if date_time < Utc::now() {
            Err(SchedulingInPastError)
        } else {
            self.at = date_time;
            Ok(self)
        }
    }
}

也許我們想要的不是絕對時間,而是未來某個時刻的相對時間:

impl<T: FnOnce() -> ()> FutureRequestBuilder<T> {
    fn after(&mut self, duration: std::time::Duration) -> &mut Self {
        self.at = Utc::now() + chrono::Duration::from_std(duration).unwrap();
        self
    }
}

封裝數據

有時,我們希望對用戶隱藏一些字段:

struct Query {
    pub on_database: String,
    // ...
}
fn foo(query: &mut Query) {
    // You want mutable access to call mutable methods on the query 
    // but want to prevent against:
    query.on_database.drain(..);
}

所以你可以將字段設置爲私有,並創建一個函數來構造值 (稱爲構造函數):

impl Query {
    fn new(
        fields: Vec<String>,
        text_filter: String,
        database: String,
        table: String,
        fixed_amount: Option<usize>,
        descending: bool,
    ) -> Self {
        unimplemented!()
    }
}
let query = Query::new(
    vec!["title".into()],
    "Morbius 2".into(),
    "imdb".into(),
    "films".into(),
    None,
    false
);

但是,這在調用的時候比較困惑,“imdb” 究竟是一個數據庫、表還是一個字段。

而構造器模式在初始化時就是非常易讀和容易理解的:

let query = Query::builder()
    .fields(vec!["title".into()]),
    .text_filter("Morbius 2".into()),
    .database("imdb".into()),
    .table("films".into()),
    .fixed_amount(None),
    .descending(false)
    .build();

枚舉和嵌套數據

到目前爲止,我們只討論了結構體,現在讓我們來談談枚舉:

這裏有與每個變體相關聯的構造器:

HTMLNode::text_builder()
    .text("Some text".into())
    .build()
// vs
HTMLNode::Text("Some text".into())
// --
HTMLNode::element_builder()
    .tag_name("p".into())
    .attribute("class".into(), "big quote".into())
    .attribute("tabindex".into(), "5".into())
    .content("Some text")
// vs
HTMLNode::Element(HTMLElement {
    tag_name: "p".into(),
    attributes: [
        ("class".into(), "big quote".into()),
        ("tabindex".into(), "5".into())
    ].into_iter(),
    children: vec![HTMLNode::Text("Some text".into())]
})

構建我們自己的構造器

現在讓我們構建我們自己的構造器,例子:

#[derive(Debug)]
struct User {
    username: String,
    birthday: NaiveDate,
}
struct UserBuilder {
    username: Option<String>,
    birthday: Option<NaiveDate>,
}
#[derive(Debug)]
struct InvalidUsername;
#[derive(Debug)]
enum IncompleteUserBuild {
    NoUsername,
    NoCreatedOn,
}
impl UserBuilder {
    fn new() -> Self {
        Self {
            username: None,
            birthday: None,
        }
    }
    fn set_username(&mut self, username: String) -> Result<&mut Self, InvalidUsername> {
        // true if every character is number of lowercase letter in English alphabet
        let valid = username
            .chars()
            .all(|chr| matches!(chr, 'a'..='z' | '0'..='9'));
        if valid {
            self.username = Some(username);
            Ok(self)
        } else {
            Err(InvalidUsername)
        }
    }
    fn set_birthday(&mut self, date: NaiveDate) -> &mut Self {
        self.birthday = Some(date);
        self
    }
    fn build(&self) -> Result<User, IncompleteUserBuild> {
        if let Some(username) = self.username.clone() {
            if let Some(birthday) = self.birthday.clone() {
                Ok(User { username, birthday })
            } else {
                Err(IncompleteUserBuild::NoCreatedOn)
            }
        } else {
            Err(IncompleteUserBuild::NoUsername)
        }
    }
}

下一篇文章我們將介紹一些自動幫我們生成構造器的 crates。

本文翻譯自:

https://www.shuttle.rs/blog/2022/06/09/the-builder-pattern

coding 到燈火闌珊 專注於技術分享,包括 Rust、Golang、分佈式架構、雲原生等。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/t_MABexVACuqbx4JMgc0hA