龙空技术网

Rust Web编程:第二章 用 Rust 设计你的 Web 应用程序

启辰8 36

前言:

现时兄弟们对“javaweb项目文件路径怎么写”大约比较关怀,姐妹们都想要知道一些“javaweb项目文件路径怎么写”的相关内容。那么小编在网上收集了一些有关“javaweb项目文件路径怎么写””的相关资讯,希望小伙伴们能喜欢,咱们快快来学习一下吧!

我们之前探索了 Rust 的语法,使我们能够解决内存管理问题并构建数据结构。 然而,正如任何经验丰富的工程师都会告诉您的那样,跨多个文件和目录构建代码是构建软件的一个重要方面。

在本章中,我们将构建一个基本的命令行待办事项程序。 我们使用 Rust 的 Cargo 管理构建命令行程序所需的依赖项。 我们的程序将以可扩展的方式构建,我们构建和管理自己的模块,这些模块将导入到程序的其他领域并使用。 我们将通过构建一个跨多个文件的待办事项应用程序来学习这些概念,这些文件用于创建、编辑和删除待办事项应用程序。 该应用程序将在本地保存我们的多个待办事项应用程序文件,并且我们将能够使用命令行界面与我们的应用程序进行交互。

在本章中,我们将讨论以下主题:

使用 Cargo 管理软件项目

构建代码

与环境互动

到本章结束时,您将能够使用 Rust 构建可打包和使用的应用程序。 您还可以在代码中使用第三方包。 因此,如果您了解要解决的问题并将其分解为逻辑块,您将能够构建任何不需要服务器或图形用户界面的命令行应用程序。

技术要求

当我们转向使用 Rust 构建 Web 应用程序时,我们将不得不开始依赖第三方软件包来为我们完成一些繁重的工作。 Rust 通过一个名为 Cargo 的包管理器来管理依赖项。 要使用 Cargo,我们必须通过以下 URL 在我们的计算机上安装 Rust:。

此安装提供了编程语言 Rust 和依赖项管理器 Cargo。

使用 Cargo 管理软件项目

在开始使用 Cargo 构建程序之前,我们应该构建一个基本的单文件应用程序。 为此,我们首先必须在本地目录中创建一个名为 hello_world.rs 的文件。 .rs 扩展名表示该文件是 Rust 文件。 老实说,扩展名是什么并不重要。 如果该文件中编写了可行的 Rust 代码,编译器将毫无问题地编译并运行它。 然而,具有不同的扩展名可能会让其他开发人员和代码编辑者感到困惑,并在从其他 Rust 文件导入代码时导致问题。 因此,命名 Rust 文件时最好使用 .rs。 在我们的 hello_world.rs 文件中,我们可以有以下代码:

fn main() {    println!("hello world");}

这与上一章中的第一个代码块没有什么不同。 现在我们已经在 hello_world.rs 文件中定义了入口点,我们可以使用以下命令编译该文件:

rustc hello_world.rs

编译完成后,同目录下会生成一个可以运行的二进制文件。 如果我们在 Windows 上编译它,我们可以使用以下命令运行二进制文件:

.\hello_world.exe

如果我们在Linux或macOS上编译它,我们可以使用以下命令运行它:

./hello_world

因为我们只构建了一个简单的 hello world 示例,所以 hello world 只会被打印出来。 虽然这在在一个文件中构建简单的应用程序时很有用,但不建议用于管理跨多个文件的程序。 甚至在依赖第三方模块时也不建议这样做。 这就是 Cargo 的用武之地。Cargo 可以通过几个简单的命令来管理一切,包括运行、测试、文档、构建/编译和第三方模块依赖项。 我们将在本章中介绍这些命令。 从我们在运行 hello world 示例时所看到的情况来看,我们必须先编译代码才能运行它,所以现在让我们继续下一部分,使用 Cargo 构建一个基本应用程序。

用Cargo构建程序

使用 Cargo 进行构建非常简单。 我们所要做的就是导航到要构建项目的目录并运行以下命令:

cargo new web_app

前面的命令构建了一个基本的 Cargo Rust 项目。 如果我们探索这个应用程序,我们将看到以下结构:

└── web_app    ├── Cargo.toml    └── src         └── main.rs

我们可以看到只有一个 Rust 文件,这就是 src 目录中的 main.rs 文件。 如果打开 main.rs 文件,您将看到它与我们在上一节中创建的文件相同。 它是一个入口点,默认代码将 hello world 打印到控制台。 我们项目的依赖项和元数据在 Cargo.toml 文件中定义。 如果我们想运行我们的程序,我们不需要导航到 main.rs 文件并运行 rustc。 相反,我们可以使用 Cargo 并使用以下命令运行它:

 cargo run

当您执行此操作时,您将看到项目编译并运行并显示以下打印输出:

  Compiling web_app v0.1.0 (/Users/maxwellflitton/Documents/   github/books/Rust-Web_Programming-two/chapter02/web_app)    Finished dev [unoptimized + debuginfo] target(s) in 0.15s     Running `target/debug/web_app`hello world

您的打印输出会略有不同,因为基本目录会有所不同。 在底部,你会看到hello world,这正是我们所期望的。 我们还可以看到打印输出表明编译未优化并且它正在 target/debug/web_app 中运行。 我们可以直接导航到 target/debug/web_app 二进制文件并运行它,就像我们在上一节中所做的那样,因为这是存储二进制文件的位置。 目标目录是用于编译、运行和记录程序的文件所在的位置。 如果我们将代码附加到 GitHub 存储库,则必须将目标目录放入 .gitignore 文件中,以确保 GitHub 忽略目标目录。 现在,我们正在运行未优化的版本。 这意味着编译速度较慢但较快。 这是有道理的,因为当我们开发时,我们将进行多次编译。 但是,如果我们想运行优化版本,我们可以使用以下命令:

cargo run --release

前面的命令为我们提供了以下打印输出:

    Finished release [optimized] target(s) in 2.63s     Running `target/release/web_app`hello world

在前面的输出中,我们可以看到优化的二进制文件位于 target/release/web_app 路径中。 现在我们已经完成了基本构建,我们可以开始使用 Cargo 来利用第三方 crate。

第三方项目依赖

第三方库称为 crate。 添加它们并使用 Cargo 管理它们非常简单。 在本节中,我们将利用 rand 箱来探索此过程,该crate位于 。 必须注意的是,这个crate的文档清晰且结构良好,包含结构、特征和模块的链接。 这并不是crate本身的特性。 这是 Rust 的标准文档,我们将在下一节中介绍。 要在我们的项目中使用此包,我们打开 Cargo.toml 文件并在 [dependencies] 部分下添加 rand 包,如下所示:

[dependencies]rand = "0.7.3"

现在我们已经定义了依赖项,我们可以使用 rand crate构建随机数生成器:

use rand::prelude::*;fn generate_float(generator: &mut ThreadRng) -> f64 {    let placeholder: f64 = generator.gen();    return placeholder * 10.0}fn main() {    let mut rng: ThreadRng = rand::thread_rng();    let random_number = generate_float(&mut rng);    println!("{}", random_number);}

在前面的代码中,我们定义了一个名为generate_float的函数,它使用crate生成并返回0到10之间的浮点数。完成此操作后,我们将打印该数字。 rand 箱的实现由 rand 文档处理。 我们的 use 语句导入 rand 箱。 当使用 rand create 生成浮点数时,文档告诉我们从 rand::prelude 模块导入 (*),这简化了常见项目的导入,如 上的 crate 文档所示。 github.io/rand/rand/prelude/index.html。

ThreadRng 结构是一个随机数生成器,可生成 0 到 1 之间的 f64 值,rand 箱文档对此进行了详细说明,网址为 。 html。

现在,我们可以看到文档的力量。 只需点击 rand 文档的介绍页面,我们就可以深入了解演示中使用的结构体和函数的声明。 现在我们的代码已经构建完成,我们可以使用 Cargo run 命令来运行我们的程序了。 当 Cargo 编译时,它从 rand 箱中提取代码并将其编译为二进制文件。 我们还可以注意到现在有一个 Cargo.lock 文件。 我们知道,cargo.toml 是用来描述我们自己的依赖项的,cargo.lock 是由 Cargo 生成的,我们不应该自己编辑它,因为它包含有关我们的依赖项的准确信息。 这种无缝的功能与易于使用的文档相结合,展示了 Rust 如何通过开发生态系统的边际收益以及语言的质量来改进开发过程。 然而,所有这些从文档中获得的收益并不完全依赖于第三方库; 我们还可以自动生成我们自己的文档。

文档

速度和安全性并不是选择 Rust 等新语言进行开发的唯一好处。多年来,软件工程社区不断学习和成长。 简单的事情(例如良好的文档)可以决定项目的成败。 为了演示这一点,我们可以使用以下代码在 Rust 文件中定义 Markdown 语言:

/// This function generates a float number using a number/// generator passed into the function.////// # Arguments/// * generator (&mut ThreadRng): the random number/// generator to generate the random number////// # Returns/// (f64): random number between 0 -> 10fn generate_float(generator: &mut ThreadRng) -> f64 {    let placeholder: f64 = generator.gen();    return placeholder * 10.0}

在前面的代码中,我们用 /// 标记表示 Markdown。 这做了两件事:它告诉其他查看代码的开发人员该函数的作用,并在我们的自动生成中呈现 Markdown。 在运行 document 命令之前,我们可以定义并记录基本用户结构和基本用户特征,以显示如何记录它们:

/// This trait defines the struct to be a user.trait IsUser {    /// This function proclaims that the struct is a user.    ///    /// # Arguments    /// None    ///    /// # Returns    /// (bool) true if user, false if not    fn is_user() -> bool {        return true    }}/// This struct defines a user////// # Attributes/// * name (String): the name of the user/// * age (i8): the age of the userstruct User {    name: String,    age: i8}

现在我们已经记录了一系列不同的结构,我们可以使用以下命令运行自动记录过程:

cargo doc --open

我们可以看到文档的呈现方式与 rand 箱相同:

图 2.1 – Web 应用程序的文档视图

在前面的屏幕截图中,我们可以看到 web_app 是一个 crate。 我们还可以看到涉及 rand crate 的文档(如果我们查看屏幕截图的左下角,我们可以在 web_app crate 文档上方看到 rand crate 文档)。 如果我们点击 User 结构体,我们可以看到该结构体的声明、我们为属性编写的 Markdown 以及特征含义,如下图所示:

图 2.2 – 结构体文档

必须注意的是,在本书的后续章节中,我们不会在代码片段中包含 Markdown 以保持可读性。 不过,本书的 GitHub 存储库中提供了 Markdown 记录的代码。 现在我们已经有了一个记录良好、正在运行的 Cargo 项目,我们需要能够将参数传递给它,以便根据上下文运行不同的配置。

与cargo交互

现在我们的程序已经运行并使用了第三方模块,我们可以开始通过命令行输入与 Rust 程序进行交互。 为了使我们的程序根据上下文具有一定的灵活性,我们需要能够将参数传递到程序中并跟踪程序运行时的参数。 我们可以使用 std(标准库)标识符来做到这一点:

use std::env;fn main() {    let args: Vec<String> = env::args().collect();    println!("{:?}", args);}

在前面的代码中,我们可以看到我们将传递到程序中的参数收集到一个向量中,然后在调试模式下打印出这些参数。 让我们运行以下命令:

cargo run one two three

运行上述命令会给出以下打印输出:

["target/debug/interacting_with_cargo", "one", "two", "three"]

在这里,我们可以看到我们的 args 向量包含我们传入的参数。这并不奇怪,因为许多其他语言也接受通过命令行传递到程序中的参数。 我们还必须注意,还包括二进制文件的路径。 在这里,我还必须强调,我正在使用一个名为 interacting_with_cargo 的不同项目,因此是 target/debug/interacting_with_cargo 路径。 我们还可以从命令行参数中看到我们正在调试模式下运行。 让我们尝试使用以下命令运行程序的发行版本:

cargo run --release one two three

我们将收到以下打印输出:

["target/release/interacting_with_cargo", "one", "two", "three"]

从前面的输出中,我们可以看到 --release 不在我们的向量中。 然而,这确实给了我们一些额外的功能可以使用。 例如,我们可能希望根据编译类型运行不同的进程。 这可以通过以下代码轻松完成:

let args: Vec<String> = env::args().collect();let path: &str = &args[0];if path.contains("/debug/") {    println!("Debug is running");}else if path.contains("/release/") {    println!("release is running");}else {    panic!("The setting is neither debug or release");}

然而,前面的简单解决方案是不完整的。 仅当我们运行 Cargo 命令时,我们提取的路径才一致。 虽然 Cargo 命令非常适合构建、编译和记录,但在生产中携带所有这些文件是没有意义的。 事实上,提取静态二进制文件、将其完全包装在 Docker 容器中并直接运行二进制文件是有优势的,因为这可以将 Docker 映像的大小从 1.5 GB 减少到 200 MB。 因此,虽然这看起来像是一个快速的胜利,但它可能会导致在部署我们的应用程序时破坏代码。 因此,有必要在最后放入恐慌宏,以防止它在您不知情的情况下进入生产环境。

到目前为止,我们已经传入了一些基本命令; 然而,这没有帮助,也没有可扩展性。 还会有很多为我们编写的样板代码来为用户实现帮助指南。 为了扩展我们的命令行界面,我们可以依靠 clap crate 来处理传递到程序中的参数,并具有以下依赖项:

[dependencies]clap = "3.0.14"

为了充实我们对命令行界面的理解,我们可以开发一个玩具应用程序,它只接受一些命令并将其打印出来。 为此,我们必须使用以下代码从 main.rs 文件中的 clap crate 导入所需内容:

use clap::{Arg, App};

我们的应用程序在主函数中包含有关应用程序的元数据,代码如下:

fn main() {    let app = App::new("booking")        .version("1.0")        .about("Books in a user")        .author("Maxwell Flitton");

如果我们查看 clap 的文档,我们可以将参数直接绑定到 App 结构体; 然而,这可能会变得丑陋且紧密结合。 相反,我们将在下一步中单独定义它们。

在我们的玩具应用程序中,我们输入名字、姓氏和年龄,其定义如下:

    let first_name = Arg::new("first name")        .long("f")        .takes_value(true)        .help("first name of user")        .required(true);    let last_name = Arg::new("last name")        .long("l")        .takes_value(true)        .help("first name of user")        .required(true);    let age = Arg::new("age")        .long("a")        .takes_value(true)        .help("age of the user")        .required(true);

我们可以看到我们可以继续堆叠参数。 现在,他们不受任何东西的束缚。 现在,我们可以继续将它们绑定到我们的应用程序并在下一步中传递参数。

绑定、获取和解析输入可以通过以下代码实现:

     let app = app.arg(first_name).arg(last_name).arg(age);    let matches = app.get_matches();    let name = matches.value_of("first name")        .expect("First name is required");    let surname = matches.value_of("last name")        .expect("Surname is required");    let age: i8 = matches.value_of("age")        .expect("Age is required").parse().unwrap();        println!("{:?}", name);    println!("{:?}", surname);    println!("{:?}", age);}

现在我们有了一个如何传递命令行参数的工作示例,我们可以通过运行以下命令与应用程序交互以查看它的显示方式:

cargo run -- --help  

中间的 --help 之前的内容告诉 Cargo 将 --help 之后的所有参数传递给 clap,而不是 Cargo。 前面的命令将为我们提供以下打印输出:

booking 1.0Maxwell FlittonBooks in a userUSAGE:    interacting_with_cargo --f <first name> --l <last name>                            --a <age>OPTIONS:        --a <age>           age of the user        --f <first name>    first name of user    -h, --help              Print help information        --l <last name>     first name of user    -V, --version           Print version information

在前面的输出中,我们可以看到如何直接与编译的二进制文件进行交互。 我们还有一个很好的帮助菜单。 要与 Cargo 交互,我们需要运行以下命令:

cargo run -- --f first --l second --a 32

前面的命令将给出以下打印输出:

"first""second"32

我们可以看到解析是有效的,因为我们有两个字符串和一个整数。 像 clap 这样的 crate 之所以有用,是因为它们本质上是自我记录的。 开发人员可以查看代码并了解正在接受哪些参数,并查看它们周围的元数据。 用户只需传入帮助参数即可获得有关输入的帮助。 这种方法降低了文档过时的风险,因为它嵌入到执行它的代码中。 如果您接受命令行参数,建议您使用 crate 之类的包来实现此目的。 现在我们已经探索了构建命令行界面以使其可以扩展,我们可以在下一节中研究在多个文件上构建代码以扩展它。

构建代码

我们现在可以开始构建 Web 应用程序的旅程了。 在本章的其余部分中,我们不会接触 Web 框架或构建 HTTP 侦听器。 这将在下一章中发生。 但是,我们将构建一个与 JSON 文件交互的待办事项模块。 它将以这样的方式构建,即可以将其插入到我们以最少的努力构建的任何 Web 应用程序中。 这个待办事项模块将使我们能够创建、更新和删除待办事项。 然后我们将通过命令行与之交互。 这里的过程是探索如何构建结构良好、可扩展且灵活的代码。 为了理解这一点,我们将该模块的构建分解为以下几个部分:

为待办事项和已完成的待办事项构建结构。

构建一个工厂,使结构能够以最少的干净输入构建在模块中。

构建使结构能够删除、创建、编辑和获取待办事项的特征。

构建一个读写文件模块来存储待办事项(我们将在后面的章节中用适当的数据库替换它)。

构建一个配置模块,该模块可以根据配置文件中的变量更改应用程序的行为。

在开始执行这些步骤之前,我们需要运行应用程序。 我们可以通过导航到要放置此应用程序的所需目录并启动一个名为 todo_app 的新 Cargo 项目来完成此操作。 完成此操作后,我们将把处理待办事项管理的逻辑放入 to_do 模块中。 这可以通过创建一个 to_do 目录并将 mod.rs 文件放在该目录中来实现,如下所示:

├── main.rs└── to_do    ├── mod.rs

有了这个结构,我们就可以从结构开始构建我们的 to_do 模块。 现在不用担心 to_do 文件,因为这已在第一步中介绍,即为已完成和待处理的待办事项构建结构。

构建待办事项结构

现在,我们只有两个待办事项结构:等待完成的结构和已经完成的结构。 但是,我们可能想引入其他类别。 例如,我们可以为已启动但由于某种原因被阻止的任务添加待办事项类别或暂停任务。 为了避免错误和重复代码,我们可以构建一个 Base 结构并让其他结构使用它。 Base 结构体包含公共字段和函数。 基本结构的更改将传播到所有其他待办事项结构。 我们还需要定义待办事项的类型。 我们可以在字符串中硬编码待处理和完成; 然而,这是不可扩展的并且也容易出错。 为了避免这种情况,我们将使用枚举来分类和定义待办事项类型的表示。 为此,我们需要为我们的模块创建以下文件结构:

├── main.rs└── to_do    ├── enums.rs    ├── mod.rs    └── structs        ├── base.rs        ├── done.rs        ├── mod.rs        └── pending.rs

在前面的代码中,我们可以注意到有两个 mod.rs 文件。 这些文件本质上是我们声明文件的地方,也是我们在其中定义的内容,以便同一目录中的其他文件可以访问它们。 如果我们在 mod.rs 文件中公开声明这些文件,我们还可以允许在目录外部访问这些文件。 在我们编写任何代码之前,我们可以在图 2.3 中看到数据在我们的模块中如何流动:

图 2.3 – 待办事项模块中的数据流

我们看到我们的 Base 结构被其他待办事项结构使用。 如果我们没有声明它,其他待办事项结构将无法访问 Base 结构。 但是,to_do/structs 目录之外的文件没有引用 Base 结构,因此它不必是公共声明。

现在我们了解了模块的数据流,我们需要回顾图 2.3 并弄清楚我们首先需要做什么。 我们可以看到我们的枚举没有依赖关系。 事实上,我们的枚举提供了所有的结构。 因此,我们将从 /to_do/enums.rs 文件中的枚举开始。 我们的枚举使用以下代码定义任务的状态:

pub enum TaskStatus {    DONE,    PENDING}

当涉及定义任务状态时,这将在代码中起作用。 但是,如果我们想要写入文件或数据库,我们将必须构建一个方法来使我们的枚举能够以字符串格式表示。 为此,我们可以使用以下代码为 TaskStatus 枚举实现 stringify 函数:

impl TaskStatus {    pub fn stringify(&self) -> String {        match &self {            &Self::DONE => {"DONE".to_string()},            &Self::PENDING => {"PENDING".to_string()}        }    }}

调用此函数将使我们能够在控制台中打印出待办任务的状态并将其写入 JSON 文件中。

笔记

虽然 stringify 函数可以工作,但还有另一种方法可以将枚举值转换为字符串。 为了实现字符串转换,我们可以实现 TaskStatus 的 Display 特征。 首先,我们必须使用以下代码导入格式模块:

use std::fmt;

然后,我们可以使用以下代码实现 TaskStatus 结构的 Display 特征:

impl fmt::Display for TaskStatus {    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {        match &self {            &Self::DONE => {write!(f, "DONE")},            &Self::PENDING => {write!(f, "PENDING")}        }    }}

此特征实现与我们的 stringify 函数具有相同的逻辑。 然而,我们的特质会在需要的时候被利用。 所以,我们有以下代码:

println!("{}", TaskStatus::DONE);println!("{}", TaskStatus::PENDING);let outcome = TaskStatus::DONE.to_string();println!("{}", outcome);

这将导致以下打印输出:

DONEPENDINGDONE

在这里,我们可以看到,当我们将 TaskStatus 传递到 println! 时,会自动利用 Display 特征。

现在,我们可以使用以下代码在 /to_do/mod.rs 文件中公开我们的枚举:

pub mod enums;

现在我们可以参考图 2.3 来看看接下来可以构建什么,即 Base 结构体。 我们可以使用以下代码在 /to_do/structs/base.rs 文件中定义 Base 结构体:

use super::super::enums::TaskStatus;pub struct Base {    pub title: String,    pub status: TaskStatus}

从文件顶部的导入中,我们可以使用 super::super 访问 TaskStatus 枚举。 我们知道 TaskStatus 枚举位于更高的目录中。 由此,我们可以推断出 super 使我们能够访问当前目录的 mod.rs 文件中声明的内容。 因此,在 /to_do/structs/ 目录中的文件中使用 super::super 可以让我们访问 /to_do/mod.rs 文件中定义的内容。

现在,我们可以使用以下代码在 /to_do/structs/mod.rs 文件中声明我们的 Base 结构:

mod base;

我们不必将其声明为公共,因为在 /to_do/structs/ 目录之外无法访问我们的 Base 结构。 现在,回顾图 2.3,我们可以构建 Pending 和 Done 结构体。 这是当我们使用组合来利用 /to_do/structs/pending.rs 文件中的 Base 结构时,代码如下:

use super::base::Base;use super::super::enums::TaskStatus;pub struct Pending {    pub super_struct: Base}impl Pending {    pub fn new(input_title: &str) -> Self {        let base = Base{            title: input_title.to_string(),            status: TaskStatus::PENDING        };        return Pending{super_struct: base}    }}

通过前面的代码,我们可以看到我们的 super_struct 字段容纳了我们的 Base 结构体。 我们利用枚举并将状态定义为待处理。 这意味着我们只需将标题传递给构造函数,并且我们有一个带有标题和待处理状态的结构。 考虑到这一点,使用以下代码在 /to_do/structs/done.rs 文件中编写 Done 结构应该很简单:

use super::base::Base;use super::super::enums::TaskStatus;pub struct Done {    pub super_struct: Base}impl Done {    pub fn new(input_title: &str) -> Self {        let base = Base {            title: input_title.to_string(),            status: TaskStatus::DONE        };        return Done{super_struct: base}    }}

我们可以看到,除了 TaskStatus 枚举具有 DONE 状态之外,与 Pending 结构体定义没有太大区别。 现在,我们可以使用以下代码使结构在 /to_do/structs/mod.rs 文件中的目录外部可用:

mod base;pub mod done;pub mod pending;

然后,我们可以通过使用以下代码在 /to_do/mod.rs 文件中声明这些结构,使我们的结构在 main.rs 文件中可访问:

pub mod structs;pub mod enums;

我们现在已经创建了一个基本模块并将其暴露给 main.rs 文件。 现在,我们可以编写一些基本代码,使用我们的模块来创建一个待处理的任务和另一个已完成的任务。 这可以通过以下代码完成:

mod to_do;use to_do::structs::done::Done;use to_do::structs::pending::Pending;fn main() {    let done = Done::new("shopping");    println!("{}", done.super_struct.title);    println!("{}", done.super_struct.status.stringify());    let pending = Pending::new("laundry");    println!("{}", pending.super_struct.title);    println!("{}", pending.super_struct.status.stringify()    );}

在前面的代码中,我们可以看到我们已经声明了 to_do 模块。 然后我们导入结构并创建一个待处理和完成的结构。 运行我们的代码将为我们提供以下打印输出:

shoppingDONElaundryPENDING

这可以防止 main.rs 文件因过多代码而过载。 如果我们要堆叠更多类型的可创建项目,例如暂停或积压的项目,则 main.rs 中的代码将会膨胀。 这就是工厂发挥作用的地方,我们将在下一步中进行探索。

使用工厂管理结构

工厂模式是我们在模块入口点抽象结构构造的地方。 我们可以看到这如何与我们的模块一起工作,如下所示:

图 2.4 – 待办事项工厂流程

工厂所做的就是通过提供接口来抽象模块。 虽然我们很喜欢构建我们的模块,但如果其他开发人员想要使用它,一个简单的工厂界面和良好的文档将为他们节省大量时间。 他们所要做的就是传入一些参数,并从工厂中获取封装在枚举中的构造结构。 如果我们改变模块的内部结构或者它变得更加复杂,这并不重要。 如果其他模块使用该接口,如果我们保持接口一致,则更改不会破坏其余代码。 我们可以通过使用以下代码在 /to_do/mod.rs 文件中定义工厂函数来构建我们的工厂:

pub mod structs;pub mod enums;use enums::TaskStatus;use structs::done::Done;use structs::pending::Pending;pub enum ItemTypes {    Pending(Pending),    Done(Done)}pub fn to_do_factory(title: &str,                      status: TaskStatus) -> ItemTypes {    match status {        TaskStatus::DONE => {            ItemTypes::Done(Done::new(title))        },        TaskStatus::PENDING => {            ItemTypes::Pending(Pending::new(title))        }    }}

在上面的代码中,我们可以看到我们定义了一个名为 ItemTypes 的枚举,它封装了构建的任务结构体。 我们的工厂函数本质上接受我们输入的标题和状态。 工厂然后匹配输入的状态。 一旦我们确定了传入的状态类型,我们就会构建一个与状态匹配的任务并将其包装在 ItemTypes 枚举中。 这可能会变得更加复杂,而我们的主文件也不会变得更明智。 然后我们可以使用以下代码在 main.rs 文件中实现这个工厂:

mod to_do;use to_do::to_do_factory;use to_do::enums::TaskStatus;use to_do::ItemTypes;fn main() {    let to_do_item = to_do_factory("washing",                                    TaskStatus::DONE);    match to_do_item {        ItemTypes::Done(item) => {            println!("{}", item.super_struct.status                     .stringify());            println!("{}", item.super_struct.title);        },        ItemTypes::Pending(item) => {            println!("{}", item.super_struct.status                     .stringify());            println!("{}", item.super_struct.title);        }    }}

在前面的代码中,我们可以看到我们将要为待办事项创建的参数传递到工厂中,然后匹配结果以打印该项目的属性。 现在 main.rs 文件中引入了更多代码。 但是,还有更多代码,因为我们正在解开返回的枚举以打印出属性以用于演示目的。 我们通常会将这个包装好的枚举传递给其他模块来处理。 要创建结构体,我们只需要一行代码,如下所示:

let to_do_item = to_do_factory("washing", TaskStatus::DONE);

这意味着我们可以创建一个待办事项并将其轻松传递,因为它被包装在枚举中。 其他函数和模块必须只接受枚举。 我们可以看到这提供了灵活性。 然而,正如我们的代码所示,我们可以取消 Base、Done 和 Pending 结构,而只使用一个接受状态和标题的结构。 这意味着更少的代码。 然而,它的灵活性也会较差。 我们将在下一步中看看情况如何,我们将向结构添加特征以锁定功能并确保安全。

用特征(traits)定义功能

现在,我们的结构体除了保存任务的状态和标题之外,实际上并没有做任何事情。 然而,我们的结构可以具有不同的功能。 因此,我们在定义各个结构时花费了额外的精力。 例如,我们正在此处构建一个待办事项应用程序。 最终取决于您如何构建应用程序,但确保您无法创建已完成的任务并非没有道理; 否则,你为什么要把它添加到你的待办事项列表中? 这个例子可能看起来微不足道。 本书使用待办事项列表来使我们正在解决的问题保持简单。 正因为如此,我们可以专注于用 Rust 开发 Web 应用程序的技术方面,而无需花时间理解我们正在解决的问题。

然而,我们必须承认,在要求更高的应用程序中,例如处理银行交易的系统,我们需要严格控制如何实现我们的逻辑并锁定发生任何不良过程的可能性。 我们可以在待办事项应用程序中通过为每个流程构建单独的特征并将它们分配给我们想要的任务结构来做到这一点。 为此,我们需要在 to_do 模块中创建一个特征目录,并为每个特征创建一个文件,该文件将采用以下结构:

├── mod.rs└── traits    ├── create.rs    ├── delete.rs    ├── edit.rs    ├── get.rs    └── mod.rs

然后,我们可以使用以下代码公开定义 to_do/traits/mod.rs 文件中的所有特征:

pub mod create;pub mod delete;pub mod edit;pub mod get;

我们还必须使用以下代码在 to_do/mod.rs 文件中公开定义我们的特征:

pub mod traits;

现在我们已经在模块中找到了所有的特征文件,我们可以开始构建我们的特征了。 我们可以首先使用以下代码在 to_do/traits/get.rs 文件中定义 Get 特征:

pub trait Get {    fn get(&self, title: &str) {        println!("{} is being fetched", title);    }}

这只是我们如何应用特征(trait)的演示; 因此,我们只打印出现在发生的情况。 我们必须记住,我们不能引用传入的 &self 参数中的字段,因为我们可以将我们的特征应用于多个结构; 但是,我们可以覆盖实现此功能的特征的 get 函数。 当涉及到 Edit 特征时,我们可以使用两个函数来更改 to_do/traits/edit.rs 文件中的状态,代码如下:

pub trait Edit {    fn set_to_done(&self, title: &str) {        println!("{} is being set to done", title);    }    fn set_to_pending(&self, title: &str) {        println!("{} is being set to pending", title);    }}

我们可以在这里看到一个模式。 因此,为了完整起见,我们的 Create 特征在 to_do/traits/create.rs 文件中采用以下形式:

pub trait Create {    fn create(&self, title: &str) {        println!("{} is being created", title);    }}

我们的删除特征在 to_do/traits/delete.rs 文件中定义,代码如下:

pub trait Delete {    fn delete(&self, title: &str) {        println!("{} is being deleted", title);    }}

我们现在已经定义了我们需要的所有特征。 因此,我们可以利用它们来定义和锁定待办事项结构中的行为。 对于 Done 结构体,我们可以使用以下代码将特征导入到 to_do/structs/done.rs 文件中:

use super::super::traits::get::Get;use super::super::traits::delete::Delete;use super::super::traits::edit::Edit;

然后,我们可以使用以下代码在定义 Done 结构后在同一文件中实现 Done 结构:

impl Get for Done {}impl Delete for Done {}impl Edit for Done {}

现在,我们的 Done 结构体可以获取、编辑和删除待办事项。 在这里,我们可以真正看到第 1 章“Rust 快速简介”中强调的特征的力量。 我们可以轻松地叠加或删除特征。 例如,允许创建已完成的待办事项可以通过简单的 impl Create for Done; 来实现。 现在我们已经定义了 Done 结构所需的特征,我们可以继续处理 Pending 结构,使用以下代码在 to_do/structs/pending.rs 文件中导入我们需要的内容:

use super::super::traits::get::Get;use super::super::traits::edit::Edit;use super::super::traits::create::Create;

然后,我们可以使用以下代码在定义 Pending 结构之后实现这些特征:

impl Get for Pending {}impl Edit for Pending {}impl Create for Pending {}

在上面的代码中,我们可以看到我们的 Pending 结构体可以获取、编辑和创建,但不能删除。 实现这些特征还将我们的 Pending 和 Done 结构联系在一起,而无需编译它们。 例如,如果我们接受一个实现 Edit 特征的结构,它将接受 Pending 和 Done 结构。 但是,如果我们要创建一个接受实现删除特征的结构的函数,它将接受 Done 结构但拒绝 Pending 结构。 这为我们提供了积极的类型检查和灵活性的美妙交响曲,这确实证明了 Rust 的设计。 现在我们的结构体具有我们想要的所有特征,我们可以利用它们完全重写我们的 main.rs 文件。 首先,我们使用以下代码导入我们需要的内容:

mod to_do;use to_do::to_do_factory;use to_do::enums::TaskStatus;use to_do::ItemTypes;use crate::to_do::traits::get::Get;use crate::to_do::traits::delete::Delete;use crate::to_do::traits::edit::Edit;

这导入很重要。 尽管我们已经在所需的结构上实现了特征,但我们必须将特征导入到正在使用它们的文件中。 这可能有点令人困惑。 例如,在初始化结构后从 Get 特征调用 get 函数将采用 item.get(&item.super_struct.title); 的形式。 get 函数与初始化的结构相关联。 直观上,不需要导入该特征是有道理的。 但是,如果您不导入该特征,您的编译器或 IDE 会给您一个无用的错误,即在结构中找不到名为 get 的函数。 这很重要,因为我们将来将使用数据库包和 Web 框架中的特征,并且我们需要导入这些特征才能使用包结构。 通过导入,我们可以在主函数中使用我们的特征和工厂,代码如下:

fn main() {    let to_do_items = to_do_factory("washing",                                     TaskStatus::DONE);    match to_do_items {        ItemTypes::Done(item) => {            item.get(&item.super_struct.title);            item.delete(&item.super_struct.title);        },        ItemTypes::Pending(item) => {            item.get(&item.super_struct.title);            item.set_to_done(&item.super_struct.title);        }    }}

运行前面的代码会得到以下打印输出:

washing is being fetchedwashing is being deleted

我们在这里所做的是构建我们自己的模块,其中包含一个入口点。 然后我们将其导入到 main 函数中并运行它。 现在,基本结构已构建并可以运行,但我们需要通过传递变量并写入文件来使模块与环境交互才能变得有用。

与环境互动

为了与环境互动,我们必须管理两件事。 首先,我们需要加载、保存和编辑待办事项的状态。 其次,我们还必须接受用户输入来编辑和显示数据。 我们的程序可以通过为每个进程运行以下步骤来实现这一点:

收集用户的参数。

定义命令(获取、编辑、删除和创建)并根据传递到应用程序的命令定义待办事项标题。

加载一个 JSON 文件,该文件存储以前运行程序时的待办事项。

根据传递到程序中的命令运行获取、编辑、删除或创建函数,最后将状态结果保存在 JSON 文件中。

我们可以通过最初使用 serde (crate)加载我们的状态来开始使这个四步过程成为可能。

读取和写入 JSON 文件

我们现在正处于以 JSON 文件的形式保存数据的阶段。 我们将在第 6 章“使用 PostgreSQL 进行数据持久化”中将其升级为合适的数据库。 但现在,我们将在 Web 应用程序中引入第一个依赖项,即 serde_json。 这个 serde_json 箱处理 Rust 数据到 JSON 数据的转换,反之亦然。 我们将在下一章中使用 serde_json 来处理 HTTP 请求。 我们可以使用以下代码在 Cargo.toml 文件中安装我们的箱子:

[dependencies]serde_json="1.0.59"

鉴于我们将来要升级存储选项,因此将读写 JSON 文件的操作与应用程序的其余部分分开是有意义的。 当我们将其拉出来进行数据库升级时,我们不希望进行大量的调试和重构。 我们还将保持简单,因为在读取和写入 JSON 文件时不需要管理架构或迁移。 考虑到这一点,我们所需要的只是读取和写入函数。 由于我们的模块小而简单,因此我们可以将模块放置在 main.rs 文件旁边的一个文件中。 首先,我们需要使用以下代码在 src/state.rs 文件中导入我们需要的内容:

use std::fs::File;use std::fs;use std::io::Read;use serde_json::Map;use serde_json::value::Value;use serde_json::json;

正如我们所看到的,我们需要标准库和一系列结构来读取数据,如图 2.5 所示:

图2.5-读取JSON文件的步骤

我们可以使用以下代码执行图2.5中的步骤:

pub fn read_file(file_name: &str) -> Map<String, Value> {    let mut file = File::open(file_name.to_string()).unwrap();    let mut data = String::new();    file.read_to_string(&mut data).unwrap();    let json: Value = serde_json::from_str(&data).unwrap();    let state: Map<String, Value> = json.as_object()                                    .unwrap().clone();    return state}

在前面的代码中,我们可以看到我们直接解开文件的打开部分。 这是因为如果我们无法读取文件,那么继续程序就没有意义,因此,我们直接解开读取的文件。 我们还必须注意,该字符串必须是可变的,因为我们要用 JSON 数据填充该字符串。 此外,我们使用 serde_json 箱来处理 JSON 数据并将其配置到映射中。 现在,我们可以在程序的其余部分中通过此 Map 变量访问我们的待办事项。 现在,我们需要写入数据,这可以使用以下代码在同一文件中完成:

pub fn write_to_file(file_name: &str,                      state: &mut Map<String, Value>) {    let new_data = json!(state);    fs::write(          file_name.to_string(),          new_data.to_string()    ).expect("Unable to write file");}

在前面的代码中,我们接受 Map 变量和文件路径。 然后我们使用 json! 将 Map 变量转换为 JSON! 来自我们的 serde_json 箱子的宏。 然后,我们将 JSON 数据转换为字符串,然后将其写入 JSON 文件。 因此,我们现在拥有读取待办事项并将其写入 JSON 文件的功能。 现在,我们可以升级 main.rs 文件并构建一个简单的命令行待办事项应用程序,用于读取待办事项并将其写入 JSON 文件。 我们可以使用传递到程序中的一些基本参数与以下代码进行交互:

mod state;use std::env;use state::{write_to_file, read_file};use serde_json::value::Value;use serde_json::{Map, json};fn main() {    let args: Vec<String> = env::args().collect();    let status: &String = &args[1];    let title: &String = &args[2];    let mut state: Map<String, Value> =    read_file("./state.json");    println!("Before operation: {:?}", state);    state.insert(title.to_string(), json!(status));    println!("After operation: {:?}", state);    write_to_file("./state.json", &mut state);}

在前面的代码中,我们做了以下事情:

从传递到我们程序的参数中收集状态和标题

从 JSON 文件中读取待办事项

从 JSON 文件打印出待办事项

插入了我们的新待办事项

从内存中打印出一组新的待办事项

将新的待办事项列表写入 JSON 文件

我们的根路径将是 Cargo.toml 文件所在的位置,因此我们在 Cargo.toml 文件旁边定义一个名为 state.json 的空 JSON 文件。 要与其交互,我们可以传入以下命令:

cargo run pending washing

前面的命令将产生以下打印输出:

Before operation: {}After operation: {"washing": String("pending")}

在前面的输出中,我们可以看到已插入洗涤。 对 JSON 文件的检查还会显示清洗已写入该文件。 您可能已经注意到,我们已经删除了对 to_do 模块的所有提及,包括我们构建的所有结构和特征。 我们没有忘记他们。 相反,我们只是在尝试将 to_do 与状态模块融合之前测试与 JSON 文件的交互是否有效。 我们将通过修改下一节中的待办事项结构中实现的特征来融合 to_do 和 state 模块。

重温 traits

现在我们已经定义了管理 JSON 文件中待办事项状态的模块,我们了解了特征函数如何处理数据并与 JSON 文件交互。 首先,我们可以更新最简单的特征,即 src/to_do/traits/get.rs 文件中的 Get 特征。 在这里,我们只是从 JSON 映射中获取待办事项并将其打印出来。 我们可以通过简单地将 JSON 映射传递到 get 函数中,使用状态中的待办事项标题从映射中获取待办事项状态,然后使用以下代码将其打印到控制台来完成此操作:

use serde_json::Map;use serde_json::value::Value;pub trait Get {    fn get(&self, title: &String, state: &Map<String, Value>) {        let item: Option<&Value> = state.get(title);        match item {            Some(result) => {                println!("\n\nItem: {}", title);                println!("Status: {}\n\n", result);            },            None => println!("item: {} was not found",                               title)        }    }}

在前面的代码中,我们可以看到我们在 JSON Map 上执行了 get 函数,匹配结果并打印出我们提取的内容。 这意味着任何实现 Get 特征的待办事项现在都可以从我们的状态中提取待办事项并将其打印出来。

我们现在可以继续复杂性的下一步,即 src/to_do/traits/create.rs 文件中的 Create 特征。 这比我们的 Get 特征稍微复杂一些,因为我们通过插入新的待办事项来编辑状态,然后将此更新后的状态写入 JSON。 我们可以使用以下代码执行这些步骤:

use serde_json::Map;use serde_json::value::Value;use serde_json::json;use crate::state::write_to_file;pub trait Create {    fn create(&self, title: &String, status: &String,              state: &mut Map<String, Value>) {            state.insert(title.to_string(), json!(status));            write_to_file("./state.json", state);            println!("\n\n{} is being created\n\n", title);    }}

在前面的代码中,我们可以看到我们使用了 state 模块中的 write_to_file 函数将状态保存到 JSON 文件。 我们可以使用 create 函数作为删除待办事项时需要执行的操作的模板。 删除本质上与我们在创建函数中所做的相反。 如果需要,您现在可以尝试在 src/to_do/traits/delete.rs 文件中编写删除特征的删除函数,然后再继续。 您的功能可能看起来有所不同; 但是,它的运行方式应与以下代码相同:

use serde_json::Map;use serde_json::value::Value;use crate::state::write_to_file;pub trait Delete {    fn delete(&self, title: &String,        state: &mut Map<String, Value>) {        state.remove(title);        write_to_file("./state.json", state);        println!("\n\n{} is being deleted\n\n", title);    }}

在前面的代码中,我们仅在 JSON Map 上使用删除函数,将更新后的状态写入 JSON 文件。 我们已经接近本节的结尾了。 我们现在需要做的就是在 src/to_do/traits/edit.rs 文件中为 Edit 特征构建编辑函数。 我们有两个功能。 人们会将待办事项的状态设置为“完成”。 另一个函数会将待办事项的状态设置为“待处理”。 这些将通过更新状态然后将更新后的状态写入 JSON 文件来实现。 在继续阅读之前,您可以尝试自己编写此内容。 希望您的代码类似于以下代码:

use serde_json::Map;use serde_json::value::Value;use serde_json::json;use crate::state::write_to_file;use super::super::enums::TaskStatus;pub trait Edit {    fn set_to_done(&self, title: &String,        state: &mut Map<String, Value>) {        state.insert(title.to_string(),        json!(TaskStatus::DONE.stringify()));        write_to_file("./state.json", state);        println!("\n\n{} is being set to done\n\n", title);    }    fn set_to_pending(&self, title: &String,        state: &mut Map<String, Value>) {        state.insert(title.to_string(),        json!(TaskStatus::PENDING.stringify()));        write_to_file("./state.json", state);        println!("\n\n{} is being set to pending\n\n", title);    }}

我们的特征现在可以在没有 JSON 文件的情况下进行交互,执行我们最初希望它们执行的流程。 没有什么可以阻止我们直接在 main.rs 文件中使用这些特征,就像我们第一次定义这些特征时所做的那样。 然而,这是不可扩展的。 这是因为我们本质上将构建一个具有多个视图和 API 端点的 Web 应用程序。 因此,我们将在多个不同文件中与这些特征和存储过程进行交互。 因此,我们必须想出一种以标准化方式与这些特征进行交互的方法,而不必重复代码,我们将在下一节中这样做。

处理特征和结构

为了使我们的代码能够与简单的界面交互,使我们能够以最小的痛苦进行更新并减少重复代码和错误,我们需要一个流程层,如图 2.6 所示:

图 2.6 – 通过进程模块的特征流

在图 2.6 中,我们可以看到我们的结构以松散的方式绑定到特征,并且进出 JSON 的数据流经过这些特征。 我们还可以看到,processes 模块中有一个入口点,该入口点会将命令定向到正确的特征,而特征又会在需要时访问 JSON 文件。 既然我们已经定义了我们的特征,我们需要做的就是构建我们的进程模块,将其连接到特征,然后将其连接到我们的 main.rs 文件。 我们将在单个 src/processes.rs 文件中构建整个进程模块。 我们将其保留在单个文件中,因为当我们覆盖数据库时我们将删除它。 如果我们知道将来要删除它,就没有必要承担太多的技术债务。 现在,我们可以通过使用以下代码最初导入所需的所有结构和特征来开始构建流程模块:

use serde_json::Map;use serde_json::value::Value;use super::to_do::ItemTypes;use super::to_do::structs::done::Done;use super::to_do::structs::pending::Pending;use super::to_do::traits::get::Get;use super::to_do::traits::create::Create;use super::to_do::traits::delete::Delete;use super::to_do::traits::edit::Edit;

现在我们可以开始构建非公共函数了。 我们可以从处理 Pending 结构开始。 我们知道我们可以获取、创建或编辑待办事项,如以下代码所示:

fn process_pending(item: Pending, command: String,                    state: &Map<String, Value>) {    let mut state = state.clone();    match command.as_str() {    "get" => item.get(&item.super_struct.title, &state),    "create" => item.create(&item.super_struct.title,     &item.super_struct.status.stringify(), &mut state),    "edit" => item.set_to_done(&item.super_struct.title,                                &mut state),    _ => println!("command: {} not supported", command)    }}

在前面的代码中,我们可以看到我们已经获取了待办事项的 Pending 结构、命令和当前状态。 然后我们匹配命令并执行与该命令关联的特征。 如果传入的命令既不是 get、create 也不是 edit,则我们不支持它,并抛出一个错误,告诉用户不支持哪些命令。 这是可扩展的。 例如,如果我们允许 Pending 结构从 JSON 文件中删除待办事项,我们只需为 Pending 结构实现删除特征,然后将删除命令添加到 process_pending 函数中。 总共只需要两行代码,并且此更改将在整个应用程序中生效。 如果我们删除命令,也会发生这种情况。 我们现在可以灵活地实现 Pending 结构。 考虑到这一点,您可以选择在继续阅读之前编写我们的 process_done 函数。 如果您选择这样做,希望它看起来像以下代码:

fn process_done(item: Done, command: String,                 state: &Map<String, Value>) {    let mut state = state.clone();    match command.as_str() {        "get" => item.get(&item.super_struct.title,                           &state),        "delete" => item.delete(&item.super_struct.title,                                 &mut state),        "edit" =>              item.set_to_pending(&item.super_struct.title,                                  &mut state),        _ => println!("command: {} not supported", command)    }}

我们现在可以处理我们的两个结构。 这就是设计模块时结构的可扩展性发挥作用的地方。 与命令一样,我们希望像处理特征一样堆叠我们的结构。 这就是我们的入口点出现的地方,如图 2.7 所示:

图 2.7 – 流程的可扩展性

从图 2.7 中我们可以看到,我们可以通过增加入口点的路由来扩展对结构的访问。 为了更好地理解这一点,我们应该使用以下代码定义我们的入口点,这次是一个公共函数:

pub fn process_input(item: ItemTypes, command: String,                      state: &Map<String, Value>) {    match item {        ItemTypes::Pending(item) => process_pending(item,                                     command, state),        ItemTypes::Done(item) => process_done(item,                                     command, state)    }}

在前面的代码中,我们可以看到我们使用 ItemTypes 枚举路由到正确的结构。 我们的模块可以通过将新结构添加到 ItemTypes 枚举、编写一个在进程模块中处理该结构的新函数,然后将所需的特征应用于该结构来处理更多结构。 我们的进程模块现已完全完成,我们可以重写 main.rs 文件来使用它。 首先,我们使用以下代码导入我们需要的内容:

mod state;mod to_do;mod processes;use std::env;use serde_json::value::Value;use serde_json::Map;use state::read_file;use to_do::to_do_factory;use to_do::enums::TaskStatus;use processes::process_input;

通过这些导入,我们可以看到我们将从 JSON 文件中读取数据,使用 to_do_factory 根据从环境中收集的输入创建结构,并将其传递到我们的进程模块中以更新 JSON 文件。 现在是停止阅读并尝试自己编写此过程的好时机。 请记住,您必须从 JSON 文件中获取数据,并检查待办事项的标题是否已存储在 JSON 文件中。 如果我们无法在 JSON 文件的数据中找到标题,那么我们就知道它将处于待处理状态,因为我们无法创建已完成的任务。 如果您选择这样做,您的代码有望如下所示:

fn main() {    let args: Vec<String> = env::args().collect();    let command: &String = &args[1];    let title: &String = &args[2];    let state: Map<String, Value> = read_file("./state.json");    let status: String;    match &state.get(*&title) {        Some(result) => {            status = result.to_string().replace('\"', "");        }        None=> {            status = "pending".to_owned();        }    }    let item = to_do_factory(title,                              TaskStatus::from_string(                             status.to_uppercase()));    process_input(item, command.to_string(), &state);}

在我们运行任何内容之前,您可能已经意识到我们使用 from_string 函数创建了 TaskStatus 的实例。 我们还没有构建 from_string 函数。 此时,您应该能够在 impl TaskStatus 块中自行构建它。 如果您尝试构建 from_string 函数,它应该类似于 src/to_do/enums.rs 文件中的以下代码:

impl TaskStatus {    . . .    pub fn from_string(input_string: String) -> Self {        match input_string.as_str() {            "DONE" => TaskStatus::DONE,            "PENDING" => TaskStatus::PENDING,            _ => panic!("input {} not supported",                         input_string)        }    }}

如果您已经成功地利用我们创建的接口来运行我们的程序,那么做得很好。 现在我们可以看到,我们可以轻松地在主要功能中协调一系列流程。 我们可以使用以下命令与我们的程序进行交互:

cargo run create washing

前面的命令在我们的 JSON 文件中创建了一个名为清洗的待办事项,其状态为待处理。 我们所有其他特征都受到支持,我们也可以在命令行中执行它们。 我们现在已经构建了一个基本的命令应用程序,它将待办事项存储在 JSON 文件中。 然而,它不仅仅是一个基本的命令行应用程序。 我们构建了我们的模块,使其具有可扩展性和灵活性。

概括

本章中我们实质上所做的就是构建一个程序,该程序接受一些命令行输入,与文件交互,并根据该文件中的命令和数据对其进行编辑。 数据很简单:标题和状态。 我们可以在主函数中使用多个 match 语句以及 if、else if 和 else 块来完成这一切。 然而,这是不可扩展的。 相反,我们构建了继承其他结构的结构,然后实现了特征。 然后,我们将这些结构的构造打包到一个工厂中,使其他文件能够在一行代码中使用所有这些功能。

然后,我们构建了一个处理接口,以便可以处理命令输入、状态和结构,使我们能够叠加额外的功能并通过几行代码更改流程的流程。 我们的主函数必须只专注于收集命令行参数并协调何时调用模块接口。 我们现在已经探索并利用了 Rust 管理模块的方式,为我们提供了构建现实世界程序的构建块,这些程序可以解决问题并添加功能,而不会受到技术债务和不断膨胀的主要功能的损害。 现在我们可以做到这一点,我们准备开始构建可以增长的可扩展 Web 应用程序。 在下一章中,我们将学习 Actix Web 框架来启动并运行基本的 Web 服务器。

问题

Cargo 中的 --release 参数添加到构建和运行时会做什么?

我们如何使文件能够在模块内部和外部访问?

具有单一范围的特征有什么优点?

我们必须采取哪些步骤来添加仅允许获取和编辑功能的 OnHold 待办事项?

工厂函数有什么好处?

我们如何根据某些流程有效地映射一系列流程?

答案

在构建中,--release 参数以优化的方式编译程序,而不是调试编译。 在运行中,--release 参数指向优化的二进制文件,而不是调试二进制文件。 优化的二进制文件需要更长的时间来编译,但运行速度会更快。

为了使模块中的其他文件能够访问某个文件,我们必须在模块根目录下的 mod.rs 文件中将该文件定义为模块。 我们在定义之前添加 mod 以使其可以在模块外部访问。

单作用域特征在定义结构时提供了最大的灵活性。 一个很好的例子是添加 OnHold 待办事项。 对于这个项目,我们可能只允许它具有编辑特征,我们可以通过实现单范围编辑特征来做到这一点。 如果我们有一个特征可以完成所有功能,那么这是不可能的。

在继承自基本结构的结构中,在其自己的文件中定义一个结构,该结构还实现了 Get 和 Edit 特征。 将保留类型添加到工厂文件中的枚举中。 在指向处理 OnHold 项的新函数的进程中的入口点的匹配语句中添加另一行。

工厂功能标准化了结构的构造。 它还减少了仅用一行代码在模块外部构建一系列结构之一的可能性。 这可以阻止其他文件膨胀,并且不需要开发人员在模块中四处查看即可使用它。

我们使用导致其他匹配语句的匹配语句。 这使我们能够编写类似树的效果,并且没有什么可以阻止我们稍后将分支连接到链上。 图 2.7 对此进行了演示。

标签: #javaweb项目文件路径怎么写