类型驱动开发
让我们在项目中添加一个新模块,域,并在其中定义一个新的结构, SubscriberName
:
//! src/lib.rs
// [...]
pub mod domain;
//! src/domain.rs
pub struct SubscriberName(String);
SubscriberName
是一个元组结构体,它是一种新类型,包含一个 String 类型的(未命名)字段。
SubscriberName
是一个真正的新类型,而不仅仅是一个别名——它不继承 String 上的任何方法,尝试将 String 赋值给 SubscriberName
类型的变量会触发编译器错误,例如:
let name: SubscriberName = "A string".to_string();
error[E0308]: mismatched types
--> src/main.rs:10:32
|
10 | let name: SubscriberName = "A string".to_string();
| -------------- ^^^^^^^^^^^^^^^^^^^^^^ expected `SubscriberName`,
found `String`
| |
| expected due to this
For more information about this error, try `rustc --explain E0308`.
根据我们当前的定义, SubscriberName
的内部字段是私有的:它只能根据 Rust 的可见性规则从域模块内的代码访问。
一如既往,信任但要验证:如果我们尝试在订阅请求处理程序中构建一个 SubscriberName
会发生什么?
//! src/routes/subscriptions.rs
// [...]
pub async fn subscribe(form: web::Form<FormData>, pool: web::Data<PgPool>) -> HttpResponse {
let subscriber_name = crate::domain::SubscriberName(form.name.clone());
// [...]
}
编译器会报错
error[E0603]: tuple struct constructor `SubscriberName` is private
--> src/routes/subscriptions.rs:22:42
|
22 | let subscriber_name = crate::domain::SubscriberName(form.name.clone());
| ^^^^^^^^^^^^^^ private tuple struct con
structor
|
::: src/domain.rs:1:27
|
1 | pub struct SubscriberName(String);
| ------ a constructor is private if any of the fields i
s private
|
note: the tuple struct constructor `SubscriberName` is defined here
--> src/domain.rs:1:1
|
1 | pub struct SubscriberName(String);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
因此,就目前情况而言,在域模块之外构建 SubscriberName
实例是不可能的。
让我们为 SubscriberName
添加一个新方法:
//! src/domain.rs
use unicode_segmentation::UnicodeSegmentation;
pub struct SubscriberName(String);
impl SubscriberName {
/// Returns an instance of `SubscriberName` if the input satisfies all
/// our validation constraints on subscriber names.
/// It panics otherwise.
pub fn parse(s: String) -> SubscriberName {
// `.trim()` returns a view over the input `s` without trailing
// whitespace-like characters.
// `.is_empty` checks if the view contains any character.
let is_empty_or_whitespace = s.trim().is_empty();
// A grapheme is defined by the Unicode standard as a "user-perceived"
// character: `å` is a single grapheme, but it is composed of two characters
// (`a` and `̊`).
//
// `graphemes` returns an iterator over the graphemes in the input `s`.
// `true` specifies that we want to use the extended grapheme definition set,
// the recommended one.
let is_too_long = s.graphemes(true).count() > 256;
// Iterate over all characters in the input `s` to check if any of them matches
// one of the characters in the forbidden array.
let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}'];
let contains_forbidden_characters = s.chars().any(|g| forbidden_characters.contains(&g));
if is_empty_or_whitespace || is_too_long || contains_forbidden_characters {
panic!("{s} is not a valid subscriber name");
} else {
Self(s)
}
}
}
是的,你说得对——这简直就是对 is_valid_name
的厚颜无耻的复制粘贴。
不过,有一个关键的区别:返回类型。
is_valid_name
返回一个布尔值,而 parse
方法如果所有检查都成功,则会返回一个 SubscriberName
。
还有更多!
parse
是在域模块之外构建 SubscriberName
实例的唯一方法——我们
在前面几段中已经验证过这一点。
因此,我们可以断言,任何 SubscriberName
实例都将满足我们所有的验证约束。
我们已经确保 SubscriberName
实例不可能违反这些约束。
让我们定义一个新的结构体,NewSubscriber:
//! src/domain.rs
// [...]
pub struct NewSubscriber {
pub email: String,
pub name: SubscriberName,
}
pub struct SubscriberName(String);
// [...]
如果我们将 insert_subscriber
改为接受 NewSubscriber
类型的参数而不是 FormData
类型,会发生什么情况?
pub async fn insert_subscriber(
pool: &PgPool,
new_subscriber: &NewSubscriber,
) -> Result<(), sqlx::Error> {
// [...]
}
有了新的签名,我们可以确保 new_subscriber.name
非空——不可能通过传递空的订阅者名称来调用 insert_subscriber
。
我们只需查找函数参数的类型定义即可得出这个结论——我们可以再次进行本地判断,而无需去检查函数的所有调用点。
花点时间回顾一下刚刚发生的事情:我们从一组需求开始(所有订阅者名称都必须验证一些约束),我们发现了一个潜在的陷阱(我们可能会在调用 insert_subscriber 之前忘记验证输入),然后我们利用 Rust 的类型系统彻底消除了这个陷阱。
我们通过构造使一个错误的使用模式变得不可表示——它将无法编译。
这种技术被称为类型驱动开发。
类型驱动开发是一种强大的方法,它可以将我们试图在类型系统内部建模的领域的约束进行编码,并依靠编译器来确保它们得到强制执行。
我们的编程语言的类型系统越具有表达力,我们就能越严格地限制我们的代码,使其只能表示在我们所工作的领域中有效的状态。
Rust 并没有发明类型驱动开发——它已经存在了一段时间,尤其是在函数式编程社区(Haskell、F#、OCaml 等)。Rust“只是”为您提供了一个具有足够表达力的类型系统,可以充分利用过去几十年来在这些语言中开创的许多设计模式。我们刚刚展示的特定模式在 Rust 社区中通常被称为“新型模式”。
在实现过程中,我们将逐步涉及类型驱动开发,但我强烈建议您查看本章脚注中提到的一些资源: 它们是任何开发人员的宝库。