ch12-03-improving-error-handling-and-modularity.md
commit c8a9ac9cee7923422b2eceebf0375363440dbfc1
为了改善我们的程序这里有四个问题需要修复,而且他们都与程序的组织方式和如何处理潜在错误有关。
第一,main 现在进行了两个任务:它解析了参数并打开了文件。对于一个这样的小函数,这并不是一个
大问题。然而如果 main 中的功能持续增加,main 函数处理的独立任务也会增加。当函数承担了更多责
任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功
能以便每个函数就负责一个任务。
这同时也关系到第二个问题:query 和 filename 是程序中的配置变量,而像 contents 则用来执行
程序逻辑。随着 main 函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量
时,将更难以追踪每个变量的目的。最好能将配置变量组织进一个结构,这样就能使他们的目的更明确了。
第三个问题是如果打开文件失败我们使用 expect 来打印出错误信息,不过这个错误信息只是说
Something went wrong reading the file。读取文件失败的原因有多种:例如文件不存在,或者没
有打开此文件的权限。目前,无论处于何种情况,我们只是打印出” 文件读取出现错误” 的信息,这并没
有给予使用者具体的信息!
第四,我们不停地使用 expect 来处理不同的错误,如果用户没有指定足够的参数来运行程序,他们会从
Rust 得到 index out of bounds 错误,而这并不能明确地解释问题。如果所有的错误处理都位于一处,
这样将来的维护者在需要修改错误处理逻辑时就只需要考虑这一处代码。将所有的错误处理都放在一处
也有助于确保我们打印的错误信息对终端用户来说是有意义的。
让我们通过重构项目来解决这些问题。
二进制项目的关注分离
main 函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一类在 main 函
数开始变得庞大时进行二进制程序的关注分离的指导性过程。这些过程有如下步骤:
• 将程序拆分成 main.rs 和 lib.rs 并将程序的逻辑放入 lib.rs 中。
• 当命令行解析逻辑比较小时,可以保留在 main.rs 中。
• 当命令行解析开始变得复杂时,也同样将其从 main.rs 提取到 lib.rs 中。
经过这些过程之后保留在 main 函数中的责任应该被限制为:
• 使用参数值调用命令行解析逻辑
• 设置任何其他的配置
• 调用 lib.rs 中的 run 函数
• 如果 run 返回错误,则处理这个错误
这个模式的一切就是为了关注分离:main.rs 处理程序运行,而 lib.rs 处理所有的真正的任务逻辑。因为
不能直接测试 main 函数,这个结构通过将所有的程序逻辑移动到 lib.rs 的函数中使得我们可以测试他
们。仅仅保留在 main.rs 中的代码将足够小以便阅读就可以验证其正确性。让我们遵循这些步骤来重构
程序。
提取参数解析器
首先,我们将解析参数的功能提取到一个 main 将会调用的函数中,为将命令行解析逻辑移动到 src∕lib.rs
中做准备。示例 12-5 中展示了新 main 函数的开头,它调用了新函数 parse_config。目前它仍将定义在
src∕main.rs 中:
use std::env;
use std::fs;
fn main() {
let args: Vec = env::args().collect();
let (query, filename) = parse_config(&args);
// --snip–
println!(“Searching for {}”, query);
println!(“In file {}”, filename);
let contents = fs::read_to_string(filename)
.expect(“Something went wrong reading the file”);
println!(“With text:\n{}”, contents);
}
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let filename = &args[2];
(query, filename)
}
从 main 中提取出 parse_config 函数
我们仍然将命令行参数收集进一个 vector,不过不同于在 main 函数中将索引 1 的参数值赋值给变
量 query 和将索引 2 的值赋值给变量 filename,我们将整个 vector 传递给 parse_config 函数。接着
parse_config 函数将包含决定哪个参数该放入哪个变量的逻辑,并将这些值返回到 main。仍然在 main
中创建变量 query 和 filename,不过 main 不再负责处理命令行参数与变量如何对应。
这对重构我们这小程序可能有点大材小用,不过我们将采用小的、增量的步骤进行重构。在做出这些改
变之后,再次运行程序并验证参数解析是否仍然正常。经常验证你的进展是一个好习惯,这样在遇到问
题时能帮助你定位问题的成因。