建造者(Builder)模式的 Rust 實現
大家好,我是螃蟹哥。
面向對象編程中,設計模式是很火的。然而,這些年新出的語言,不完全是面向對象的。比如 Rust、Go 等。那相關的設計模式可以在這些語言中實現嗎?本文講解 Builder (建造者)模式的 Rust 實現。
我們知道,Rust 函數不支持可選參數、命名參數,也不支持函數重載。爲了克服這一限制 Rust 開發者經常應用建造者模式。它需要一些額外的編碼,但從 API 人體工程學的角度來看,它具有與命名參數和可選參數類似的效果。
01 問題簡介
考慮以下 Rust 結構體:
struct User {
email: Option<String>,
first_name: Option<String>,
last_name: Option<String>
}
在 Ruby 中,持有相同數據的類可以定義爲:
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 沒關係,我只想讓你看到,通過明確指定相關字段來顯示用戶創建實例是多麼容易:
greyblake = User.new(
email: "greyblake@example.com",
first_name: "Sergey",
)
last_name
沒傳遞,因此它會自動獲得默認值:nil
。
02 初始化 Rust 結構體
由於我們在 Rust 中沒有默認參數,因此爲了初始化此類結構,我們必須列出所有字段:
let greyblake = User {
email: Some("example@example.com".to_string()),
first_name: Some("Sergey".to_string()),
last_name: None,
}
這與 Ruby 的命名參數非常相似,但我們必須設置所有字段,即使 last_name
是 None
,你也得顯示設置。可能你覺得沒啥,但對於大型複雜的結構,可能就有點煩人了。
當然,我們可以創建一個實現構造器:new()
impl User {
fn new(
email: Option<String>,
first_name: Option<String>,
last_name: Option<String>
) -> Self {
Self { email, first_name, last_name }
}
}
這時這麼使用:
let greyblake = User::new(
Some("example@example.com".to_string()),
Some("Sergey".to_string()),
None
)
但情況變得更糟了:我們仍然必須列出所有自動的值,而且字段順序還不能變(當然,newtype 技術可以幫助我們,但這篇文章不是關於它的)。
建造者模式可以拯救我們
建造者是一個額外的結構,它提供了一個符合人體工程學的接口來設置值和構建目標結構的方法。讓我們實現 UserBuilder
以便幫助我們構建 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 }
}
}
值得注意的點:
-
建造者類似於它構建的目標結構:
UserBuilder
與User
字段相同 -
每個字段有一個 setter 函數:
email
,first_name
,last_name
-
setter 函數第一個參數是一個 builder(
mut self
),設置值,並將構建器返回。這使得可以鏈式調用 -
new()
創建具有預定義默認值的建造者(在這種情況下,所有字段值都是None
) -
build()
構建並返回目標結構User
-
它與_建造者模式_直接無關,但我們接收
impl Into<String>
而不是String
來更新 setter 的值。這使得我們的 API 更加靈活
通常爲了方便 User
會實現 builder()
函數,因此 UserBuilder
不必明確導入:
impl User {
fn builder() -> UserBuilder {
UserBuilder::new()
}
}
最終,通過建造者我們可以構建相同的 User 結構體實例:
let greyblake = User::builder()
.email("example@example.com")
.first_name("Sergey")
.build();
雖然它仍然比 Ruby 版本 User.new
代碼略多,但我們實現了目標:
-
跳過不相關的字段並隱含使用默認值
-
相關字段及其值已明確闡明
-
不再有類型噪音,對於
Option<T>
,不需要Some(...)
03 必填字段
現在假設 User
結構體有必填字段:id
和 email
,這是更接近現實生活中的例子:
struct User {
id: String,
email: String,
first_name: Option<String>,
last_name: Option<String>,
}
Buidler 不能有關於 id
和 email
合理的默認值,所以我們必須找到一種方法來傳遞它們。
而在 Ruby 中,可以強制要求 id
和 email
必填,只需要在構造函數中將其中的默認值 nil
移除即可:
class User
def initialize(id:, email:, first_name: nil, last_name: nil)
# ...
end
end
在 Rust 中,爲了解決這個問題,我們可以調整建造者的構造器以接收必填字段的值:
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)
}
}
這使我們能夠構建一個用戶,確保始終指定 id
和 email
:
let greyblake = User::builder("13", "greyblake@example.com")
.first_name("Sergey")
.build();
不幸的是,它給我們帶來了與本文開頭的建造者相同的問題:字段名稱沒有明確說明,很容易以錯誤的順序傳遞參數。
有沒有解決辦法呢?我們下篇文章見!
原文鏈接:https://www.greyblake.com/blog/2021-10-19-builder-pattern-in-rust/
完整建造者模式代碼:https://github.com/colin-kiegel/rust-derive-builder
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Awt32ecqIEB7qfEU-3I7qg