建造者(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_nameNone,你也得顯示設置。可能你覺得沒啥,但對於大型複雜的結構,可能就有點煩人了。

當然,我們可以創建一個實現構造器: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 }
    }
}

值得注意的點:

通常爲了方便 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 代碼略多,但我們實現了目標:

03 必填字段

現在假設 User 結構體有必填字段:idemail,這是更接近現實生活中的例子:

struct User {
    id: String,
    email: String,
    first_name: Option<String>,
    last_name: Option<String>,
}

Buidler 不能有關於 idemail 合理的默認值,所以我們必須找到一種方法來傳遞它們。

而在 Ruby 中,可以強制要求 idemail 必填,只需要在構造函數中將其中的默認值 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)
    }
}

這使我們能夠構建一個用戶,確保始終指定 idemail

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