建造者(Builder)模式的 Rust 实现

polarisxu

共 4638字,需浏览 10分钟

 ·

2021-10-28 19:17

面向对象编程中,设计模式是很火的。然而,这些年新出的语言,不完全是面向对象的。比如 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: nilfirst_name: nillast_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 }
    }
}

值得注意的点:

  • 建造者类似于它构建的目标结构:UserBuilderUser 字段相同
  • 每个字段有一个 setter 函数:email, first_name, last_name
  • setter 函数第一个参数是一个 builder(mut self),设置值,并将构建器返回。这使得可以链式调用
  • new()创建具有预定义默认值的建造者(在这种情况下,所有字段值都是None)
  • build() 构建并返回目标结构 User
  • 它与建造者模式直接无关,但我们接收 impl Into 而不是 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,不需要  Some(...)

03 必填字段

现在假设 User 结构体有必填字段:idemail,这是更接近现实生活中的例子:

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

Buidler 不能有关于 idemail 合理的默认值,所以我们必须找到一种方法来传递它们。

而在 Ruby 中,可以强制要求 idemail 必填,只需要在构造函数中将其中的默认值 nil 移除即可:

class User
  def initialize(id:email:first_name: nillast_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




往期推荐


我是 polarisxu,北大硕士毕业,曾在 360 等知名互联网公司工作,10多年技术研发与架构经验!2012 年接触 Go 语言并创建了 Go 语言中文网!著有《Go语言编程之旅》、开源图书《Go语言标准库》等。


坚持输出技术(包括 Go、Rust 等技术)、职场心得和创业感悟!欢迎关注「polarisxu」一起成长!也欢迎加我微信好友交流:gopherstudio


浏览 132
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报