建造者(Builder)模式的 Rust 實(shí)現(xiàn)
面向?qū)ο缶幊讨?,設(shè)計(jì)模式是很火的。然而,這些年新出的語言,不完全是面向?qū)ο蟮摹?/span>比如 Rust、Go 等。那相關(guān)的設(shè)計(jì)模式可以在這些語言中實(shí)現(xiàn)嗎?本文講解 Builder (建造者)模式的 Rust 實(shí)現(xiàn)。
我們知道,Rust 函數(shù)不支持可選參數(shù)、命名參數(shù),也不支持函數(shù)重載。為了克服這一限制 Rust 開發(fā)者經(jīng)常應(yīng)用建造者模式。它需要一些額外的編碼,但從 API 人體工程學(xué)的角度來看,它具有與命名參數(shù)和可選參數(shù)類似的效果。
01 問題簡介
考慮以下 Rust 結(jié)構(gòu)體:
struct?User?{
????email:?Option<String>,
????first_name:?Option<String>,
????last_name:?Option<String>
}
在 Ruby 中,持有相同數(shù)據(jù)的類可以定義為:
class?User
??attr_reader?:email,?:first_name,?:last_name
??def?initialize(email:?nil,?first_name:?nil,?last_name:?nil)
????@email?=?email
????@first_name?=?first_name
????@last_name?=?last_name
??end
end
不懂 Ruby 沒關(guān)系,我只想讓你看到,通過明確指定相關(guān)字段來顯示用戶創(chuàng)建實(shí)例是多么容易:
greyblake?=?User.new(
??email:?"[email protected]",
??first_name:?"Sergey",
)
last_name 沒傳遞,因此它會自動獲得默認(rèn)值:nil。
02 初始化 Rust 結(jié)構(gòu)體
由于我們在 Rust 中沒有默認(rèn)參數(shù),因此為了初始化此類結(jié)構(gòu),我們必須列出所有字段:
let?greyblake?=?User?{
????email:?Some("[email protected]".to_string()),
????first_name:?Some("Sergey".to_string()),
????last_name:?None,
}
這與 Ruby 的命名參數(shù)非常相似,但我們必須設(shè)置所有字段,即使 last_name 是 None,你也得顯示設(shè)置??赡苣阌X得沒啥,但對于大型復(fù)雜的結(jié)構(gòu),可能就有點(diǎn)煩人了。
當(dāng)然,我們可以創(chuàng)建一個(gè)實(shí)現(xiàn)構(gòu)造器:new()
impl?User?{
????fn?new(
????????email:?Option<String>,
????????first_name:?Option<String>,
????????last_name:?Option<String>
????)?->?Self?{
????????Self?{?email,?first_name,?last_name?}
????}
}
這時(shí)這么使用:
let?greyblake?=?User::new(
????Some("[email protected]".to_string()),
????Some("Sergey".to_string()),
????None
)
但情況變得更糟了:我們?nèi)匀槐仨毩谐鏊凶詣拥闹?,而且字段順序還不能變(當(dāng)然,newtype 技術(shù)可以幫助我們,但這篇文章不是關(guān)于它的)。
建造者模式可以拯救我們
建造者是一個(gè)額外的結(jié)構(gòu),它提供了一個(gè)符合人體工程學(xué)的接口來設(shè)置值和構(gòu)建目標(biāo)結(jié)構(gòu)的方法。讓我們實(shí)現(xiàn) UserBuilder 以便幫助我們構(gòu)建 User:
struct?UserBuilder?{
????email:?Option<String>,
????first_name:?Option<String>,
????last_name:?Option<String>
}
impl?UserBuilder?{
????fn?new()?->?Self?{
????????Self?{
????????????email:?None,
????????????first_name:?None,
????????????last_name:?None,
????????}
????}
????fn?email(mut?self,?email:?impl?Into<String>)?->?Self?{
????????self.email?=?Some(email.into());
????????self
????}
????fn?first_name(mut?self,?first_name:?impl?Into<String>)?->?Self?{
????????self.first_name?=?Some(first_name.into());
????????self
????}
????fn?last_name(mut?self,?last_name:?impl?Into<String>)?->?Self?{
????????self.last_name?=?Some(last_name.into());
????????self
????}
????fn?build(self)?->?User?{
????????let?Self?{?email,?first_name,?last_name?}?=?self;
????????User?{?email,?first_name,?last_name?}
????}
}
值得注意的點(diǎn):
建造者類似于它構(gòu)建的目標(biāo)結(jié)構(gòu): UserBuilder與User字段相同每個(gè)字段有一個(gè) setter 函數(shù): email,first_name,last_namesetter 函數(shù)第一個(gè)參數(shù)是一個(gè) builder( mut self),設(shè)置值,并將構(gòu)建器返回。這使得可以鏈?zhǔn)秸{(diào)用new()創(chuàng)建具有預(yù)定義默認(rèn)值的建造者(在這種情況下,所有字段值都是None)build()構(gòu)建并返回目標(biāo)結(jié)構(gòu)User它與建造者模式直接無關(guān),但我們接收 impl Into而不是String來更新 setter 的值。這使得我們的 API 更加靈活
通常為了方便 User 會實(shí)現(xiàn) builder() 函數(shù),因此 ?UserBuilder 不必明確導(dǎo)入:
impl?User?{
????fn?builder()?->?UserBuilder?{
????????UserBuilder::new()
????}
}
最終,通過建造者我們可以構(gòu)建相同的 User 結(jié)構(gòu)體實(shí)例:
let?greyblake?=?User::builder()
????.email("[email protected]")
????.first_name("Sergey")
????.build();
雖然它仍然比 Ruby 版本 User.new 代碼略多,但我們實(shí)現(xiàn)了目標(biāo):
跳過不相關(guān)的字段并隱含使用默認(rèn)值 相關(guān)字段及其值已明確闡明 不再有類型噪音,對于 Option,不需要 ?Some(...)
03 必填字段
現(xiàn)在假設(shè) User 結(jié)構(gòu)體有必填字段:id 和 email,這是更接近現(xiàn)實(shí)生活中的例子:
struct?User?{
????id:?String,
????email:?String,
????first_name:?Option,
????last_name:?Option,
}
Buidler 不能有關(guān)于 id 和 email 合理的默認(rèn)值,所以我們必須找到一種方法來傳遞它們。
而在 Ruby 中,可以強(qiáng)制要求 id 和 email 必填,只需要在構(gòu)造函數(shù)中將其中的默認(rèn)值 nil 移除即可:
class?User
??def?initialize(id:,?email:,?first_name:?nil,?last_name:?nil)
??#?...
??end
end
在 Rust 中,為了解決這個(gè)問題,我們可以調(diào)整建造者的構(gòu)造器以接收必填字段的值:
struct?UserBuilder?{
????id:?String,
????email:?String,
????first_name:?Option<String>,
????last_name:?Option<String>,
}
impl?UserBuilder?{
????fn?new(id:?impl?Into<String>,?email:?impl?Into<String>)?->?Self?{
????????Self?{
????????????id:?id.into(),
????????????email:?email.into(),
????????????first_name:?None,
????????????last_name:?None,
????????}
????}
????fn?first_name(mut?self,?first_name:?impl?Into<String>)?->?Self?{
????????self.first_name?=?Some(first_name.into());
????????self
????}
????fn?last_name(mut?self,?last_name:?impl?Into<String>)?->?Self?{
????????self.last_name?=?Some(last_name.into());
????????self
????}
????fn?build(self)?->?User?{
????????let?Self?{?id,?email,?first_name,?last_name?}?=?self;
????????User?{?id,?email,?first_name,?last_name?}
????}
}
impl?User?{
????fn?builder(id:?impl?Into<String>,?email:?impl?Into<String>)?->?UserBuilder?{
????????UserBuilder::new(id,?email)
????}
}
這使我們能夠構(gòu)建一個(gè)用戶,確保始終指定 id 和 email:
let?greyblake?=?User::builder("13",?"[email protected]")
????.first_name("Sergey")
????.build();
不幸的是,它給我們帶來了與本文開頭的建造者相同的問題:字段名稱沒有明確說明,很容易以錯(cuò)誤的順序傳遞參數(shù)。
有沒有解決辦法呢?我們下篇文章見!
原文鏈接:https://www.greyblake.com/blog/2021-10-19-builder-pattern-in-rust/
完整建造者模式代碼:https://github.com/colin-kiegel/rust-derive-builder
我是 polarisxu,北大碩士畢業(yè),曾在 360 等知名互聯(lián)網(wǎng)公司工作,10多年技術(shù)研發(fā)與架構(gòu)經(jīng)驗(yàn)!2012 年接觸 Go 語言并創(chuàng)建了 Go 語言中文網(wǎng)!著有《Go語言編程之旅》、開源圖書《Go語言標(biāo)準(zhǔn)庫》等。
堅(jiān)持輸出技術(shù)(包括 Go、Rust 等技術(shù))、職場心得和創(chuàng)業(yè)感悟!歡迎關(guān)注「polarisxu」一起成長!也歡迎加我微信好友交流:gopherstudio
