Rust之构建命令行程序(三):重构改进模块化和错误处理

开发环境

  • Windows 10
  • Rust 1.74.1

 

  • VS Code 1.85.1

项目工程

这次创建了新的工程minigrep.

重构改进模块化和错误处理 

为了改进我们的程序,我们将修复与程序结构及其处理潜在错误的方式有关的四个问题。首先,我们的main函数现在执行两项任务:解析参数和读取文件。随着我们程序的增长,main处理的独立任务的数量也会增加。随着一个功能获得更多的职责,它变得更难推理、更难测试、更难在不破坏其某个部分的情况下进行更改。最好将功能分开,每个功能负责一项任务。

这个问题还与第二个问题有关:尽管queryfile_path是程序的配置变量,但contents等变量也用于执行程序的逻辑。main越长,我们需要纳入范围的变量就越多;范围内的变量越多,就越难跟踪每个变量的用途。最好将配置变量分组到一个结构中,以使其目的明确。 

第三个问题是,我们已经使用expect在读取文件失败时打印了一条错误消息,但错误消息只是打印了Should have been able to read the file。读取文件失败的原因有很多:例如,文件可能丢失,或者我们可能没有权限打开它。现在,不管情况如何,我们都会为所有内容打印相同的错误消息,这不会给用户任何信息! 

第四,我们重复使用expect来处理不同的错误,如果用户在没有指定足够参数的情况下运行我们的程序,他们将从Rust获得一个索引越界错误,该错误无法清楚地解释问题。如果所有的错误处理代码都在一个地方是最好的,这样将来的维护人员在需要更改错误处理逻辑时就只有一个地方可以查阅代码。将所有错误处理代码放在一个地方还将确保我们打印的消息对最终用户有意义。 

让我们通过重构项目来解决这四个问题。 

二进制项目的关注点分离

将多项任务的责任分配给main功能的组织问题在许多二元项目中很常见。因此,Rust社区开发了当main开始变大时分割二进制程序的独立关注点的指导方针。该过程包括以下步骤: 

  • 将你的程序分成一个main.rs和一个lib.rs,并将你的程序逻辑转移到lib.rs。 
  • 只要您的命令行解析逻辑很小,它就可以保留在main.rs中。
  • 当命令行解析逻辑开始变得复杂时,将其从main.rs中提取出来并移动到lib.rs中。

在此流程之后,main函数的职责应仅限于以下方面: 

  • 使用参数值调用命令行解析逻辑
  • 设置其他配置
  • lib.rs中调用运行函数 
  • run返回错误时处理错误

这种模式是关于分离关注点的:main.rs处理程序的运行,lib.rs处理手头任务的所有逻辑。因为您不能直接测试main函数,所以这种结构允许您通过将程序的逻辑移入lib.rs中的函数来测试程序的所有逻辑。保留在main.rs中的代码将足够小,可以通过读取它来验证其正确性。让我们按照这个过程重新编写我们的程序。 

提取参数解析器

我们将解析参数的功能提取到一个函数中,main将调用该函数来准备将命令行解析逻辑移动到src/lib.rs .示例12-5显示了main调用新函数parse_config的新开始,我们暂时将在src/main.rs中定义该函数。 

文件名:src/main.rs

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let (query, file_path) = parse_config(&args);

    // --snip--
   println!("With value:\n{} \n{}", query, file_path);
}

fn parse_config(args: &[String]) -> (&str, &str) {
    let query = &args[1];
    let file_path = &args[2];

    (query, file_path)
}

 示例12-5 从main中提取parse_config函数

 我们仍然将命令行参数收集到一个向量中,但是我们没有将索引1处的参数值分配给变量query,也没有将索引2处的参数值分配给主函数中的变量file_path,而是将整个向量传递给parse_config函数。parse_config函数然后保存逻辑,该逻辑确定哪个参数进入哪个变量并将值传递回main。我们仍然在main中创建queryfile_path变量,但是main不再负责确定命令行参数和变量如何对应。

对于我们的小程序来说,这种返工似乎有些过头了,但我们正在以小而渐进的步骤进行重构。在做出这一更改后,再次运行程序以验证参数解析是否仍然有效。经常检查你的进展是有好处的,有助于在问题出现时找出问题的原因。

对配置值进行分组

我们可以采取另一个小步骤来进一步改进parse_config函数。目前,我们正在返回一个元组,但随后我们立即再次将该元组拆分为单独的部分。这表明我们可能还没有正确的抽象概念。

另一个显示还有改进空间的指标是parse_configconfig部分,这意味着我们返回的两个值是相关的,并且都是一个配置值的一部分。除了将这两个值分组到一个元组中之外,我们目前没有在数据结构中传达这种含义;相反,我们将把这两个值放入一个结构中,并为每个结构字段赋予一个有意义的名称。这样做将使该代码的未来维护者更容易理解不同值之间的关系以及它们的用途。 

示例12-6显示了对parse_config函数的改进。 

文件名:src/main.rs

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = parse_config(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    // --snip--
    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

fn parse_config(args: &[String]) -> Config {
    let query = args[1].clone();
    let file_path = args[2].clone();

    Config { query, file_path }
}

 示例12-6:重构

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值