【Rust自学】12.5. 重构 Pt.3:移动业务逻辑
2026/7/23 18:55:56 网站建设 项目流程

12.5 重构 Pt.3:移动业务逻辑

12.5.0. 写在正文之前

第12章要做一个实例的项目——一个命令行程序。这个程序是一个grep(Global Regular Expression Print),是一个全局正则搜索和输出的工具。它的功能是在指定的文件中搜索出指定的文字。

这个项目分为这么几步:
- 接收命令行参数
- 读取文件
-重构:改进模块和错误处理(本文)
- 使用TDD(测试驱动开发)开发库功能
- 使用环境变量
- 将错误信息写入标准错误而不是标准输出

12.5.1. 回顾

之前两节分别做了模块化的优化和错误处理,这节在此基础上还要做进一步的优化。

以下是截止到上一篇文章所写出的全部代码:

use std::env; use std::fs; use std::process; struct Config { query: String, filename: String, } fn main() { let args:Vec<String> = env::args().collect(); let config = Config::new(&args).unwrap_or_else(|err| { println!("Problem parsing arguments: {}", err); process::exit(1); }); let contents = fs::read_to_string(config.filename) .expect("Something went wrong while reading the file"); println!("With text:\n{}", contents); } impl Config { fn new(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let filename = args[2].clone(); Ok(Config { query, filename}) } }

12.5.2. 从main函数中提取逻辑

在 12.3. 重构 Pt.1 中说过二进制程序关注点分离的指导性原则
- 将程序拆分为main.rslib.rs,将业务逻辑放入lib.rs
- 当逻辑较少时,将它放在main.rs也可以
- 当逻辑变复杂时,需要将它从main.rs提取到lib.rs

根据上述拆分原则,我们应该把main函数里所有除了配置解析和错误处理之外的所有逻辑单独提取到一个run函数里。把main函数精简到足以通过阅读代码来检查正确性,而其他的逻辑就可以通过测试验证了(对于测试这部分的内容,详见 11.1. 编写和运行测试)。

对于这个截止到目前的代码,run函数应该是:

fn run(config: Config) { let contents = fs::read_to_string(config.filename) .expect("Something went wrong while reading the file"); println!("With text:\n{}", contents); }

main函数里也改为通过调用run函数来读取:

fn main() { let args:Vec<String> = env::args().collect(); let config = Config::new(&args).unwrap_or_else(|err| { println!("Problem parsing arguments: {}", err); process::exit(1); }); run(config); }

12.5.3. 改善run函数的错误处理

现在的run函数对于读取错误的情况采用的是expect。而这种错误处理会调用panic!,我们需要的是像Config::new这样使用Result类型来传播错误,就应该这么写:

fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.filename)?; println!("With text:\n{}", contents); Ok(()) }
  • Result类型的Ok对应的是()类型(单元类型),这种类型表示什么也不返回,什么也没有,因为run函数正确执行确实什么都不需要返回。这个函数体的最后一行Ok()里加了()就代表返回Ok变体,并且包裹了一个单元类型。

  • ResultErr对应的是Box<dyn Error>,这个东西你暂且不需要深入了解,只需要知道它代表所有实现了std::error::Error这个trait的类型(这里只写了Error是因为我在代码开头写了use std::error::Error;,把它引入了作用域),但是不需要指定具体的类型。这意味着在不同的场景下可以返回不同的错误类型。dyn是dynamic动态一词的简写。

  • ?这个符号在 9.3. Result枚举与可恢复的错误 Pt.2 中有详细讲过,这里就再简单讲一下:read_to_string的返回值是Result类型。加了?表示如果read_to_string的返回值是Ok,就把Ok所关联的值返回赋值给变量;如果是Err,那么会直接终止这个函数的运行,把Err及其所附带的错误信息返回。也就是说,加?的效果等同于:

let contents = match fs::read_to_string(config.filename){ Ok(contents) => contents, Err(e) => return Err(e.into()), };

e.into()会把io::Error转换成Box<dyn Error>,这样返回类型才能对上;?运算符会替你完成这种转换。)

这么改之后就会把错误传播给调用者,也就是main函数,所以在main函数里得处理可能出现的错误:

fn main() { let args:Vec<String> = env::args().collect(); let config = Config::new(&args).unwrap_or_else(|err| { println!("Problem parsing arguments: {}", err); process::exit(1); }); if let Err(e) = run(config) { println!("Application error: {}", e); process::exit(1); } }

这里使用到的if letmatch的一个语法糖,把它理解为只处理一种分支的match即可,详细可见 6.4. 简单的控制流-if let。需要强调,if letif不是同一回事,不要把它们相提并论。

12.5.4. 迁移业务逻辑

现在我们完成了所有函数的独立和错误处理,接下来要做的就是把它们移到lib.rs里。

迁移的对象就是这些函数、结构体和相关的引用。

迁移后的成果(lib.rs):

use std::error::Error; use std::fs; pub struct Config { pub query: String, pub filename: String, } impl Config { pub fn new(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let filename = args[2].clone(); Ok(Config { query, filename}) } } pub fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.filename)?; println!("With text:\n{}", contents); Ok(()) }

注意:所有的被main.rs使用的结构体、结构体上的方法和函数都得在声明时加pub关键字来声明为公共的才能被调用。

再看看main.rs:

use std::env; use std::process; use minigrep::Config; fn main() { let args:Vec<String> = env::args().collect(); let config = Config::new(&args).unwrap_or_else(|err| { println!("Problem parsing arguments: {}", err); process::exit(1); }); if let Err(e) = minigrep::run(config) { println!("Application error: {}", e); process::exit(1); } }

所有的重构任务已经完成,下一步就是编写测试(12.6. 使用TDD(测试驱动开发)开发库功能)。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询