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()
構造器組成:
-
一個生成中間構造器的函數(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)
}
}
}
-
每個 set 方法都必須接受一個可變引用,爲了持續將數據添加到構造器。
-
每個方法都必須返回一個可變引用,爲了能進行鏈式調用。
下一篇文章我們將介紹一些自動幫我們生成構造器的 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