【Rust自学】12.3. 重构 Pt.1:改善模块化

12.3.0. 写在正文之前

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

这个项目分为这么几步:

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

喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)

12.3.1. 重构的目的

重构的目的是要增进模块化的程度以及改善错误处理能力。

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

rust 复制代码
use std::env;  
use std::fs;  
  
fn main() {  
    let args:Vec<String> = env::args().collect();  
    let query = &args[1];  
    let filename = &args[2];  
  
    println!("search for {}", query);  
    println!("In file {}", filename);  
  
    let contents = fs::read_to_string(filename)  
        .expect("Somthing went wrong while reading the file");
    println!("With text:\n{}", contents);  
}

这个代码存在4个问题:

  • main函数负责的功能太多,它既负责命令行的功能解析,又负责读取文件。程序代码的编写原则是每一个函数只负责一个功能,所以说最好把函数拆开。

  • queryfilename这两个变量是用来存储程序配置的,contents是用来存储文件内容的。随着代码和变量在编写时越来越多,每个变量的实际意义就变得难以追踪。所以最好把这些变量存在结构体里。

  • 读取文件时使用expect来处理错误,不论读取时出现了什么错误都只会打印出错误信息并恐慌,这并不是最好的处理方式。因为文件读取失败可能是文件找不到,也有可能是权限问题,现在指定的这个恐慌信息"Somthing went wrong while reading the file"并不能帮助用户排查错误。

  • 如果程序里到处都使用expect方法那么用户得到的报错信息是来自于Rust语言内部的,比如"Index out of bound",不是程序员根本不明白到底是什么引发了错误。最好是将错误的代码集中放置,从而使将来的维护者在需要修改错误处理相关的逻辑时只考虑这一处代码,也能确保向用户打印的错误信息是易于理解的。

12.3.2. 二进制程序关注点分离的指导性原则

很多Rust二进制项目都会面临同样的组织结构问题,它们将过多的功能和过多的任务都放到了main函数里面。针对这种情况,Rust社区做了一套为二进制程序进行关注点分离的指导性原则:

  • 将程序拆分为main.rslib.rs,将业务逻辑放入lib.rs
  • 当逻辑较少时,将它放在main.rs也可以
  • 当逻辑变复杂时,需要将它从main.rs提取到lib.rs

经过上述拆分之后,这个例子中应该留在main函数中的功能有:

  • 使用参数值调用命令行解析逻辑
  • 进行其它配置
  • 调用lib.rs中的run函数
  • 处理run函数可能出现的问题

12.3.3. 分离逻辑

再看一眼代码:

rust 复制代码
use std::env;  
use std::fs;  
  
fn main() {  
    let args:Vec<String> = env::args().collect();  
    let query = &args[1];  
    let filename = &args[2];  
  
    println!("search for {}", query);  
    println!("In file {}", filename);  
  
    let contents = fs::read_to_string(filename)  
        .expect("Somthing went wrong while reading the file");
    println!("With text:\n{}", contents);  
}

先把获取命令行参数的部分独立出来:

rust 复制代码
fn parse_config(args: &[String]) -> (&str, &str) {  
    let query = &args[1];  
    let filename = &args[2];  
    (query, filename)  
}
  • &[String]表示是一个内部元素为StringVector切片
  • 这里没有打印queryfilename的必要了,所以就去掉

然后改一下main函数,调用parse_config

rust 复制代码
fn main() {  
    let args:Vec<String> = env::args().collect();  
    let (query, filename) = parse_config(&args);  
  
    let contents = fs::read_to_string(filename)  
        .expect("Somthing went wrong while reading the file");  
    println!("With text:\n{}", contents);  
}

12.3.4. 使用结构体

parse_config内把queryfilename组合成元组返回,在main函数里又把元组的两个值拆分为两个变量,这种来回拆分合成表明程序中建立的抽象结构有问题。

queryfilename都是配置的一部分,两者是彼此相关联的,把这两个东西放在元组里不足以表达出这种抽象的关联。最好的办法是放在结构体里:

rust 复制代码
struct Config {  
    query: String,  
    filename: String,  
}  
  
fn main() {  
    let args:Vec<String> = env::args().collect();  
    let config = parse_config(&args);  
  
    let contents = fs::read_to_string(config.filename)  
        .expect("Somthing went wrong while reading the file");  
    println!("With text:\n{}", contents);  
}  
  
fn parse_config(args: &[String]) -> Config {  
    let query = args[1].clone();  
    let filename = args[2].clone();  
    Config {  
        query,  
        filename,  
    }  
}

parse_config中必须注意queryfilename的格式:形参args的类型是&[String]是一个引用,没有所有权,所以queryfilename也是引用,但是Config这个结构体接收的是String而不是&String,所以需要通过克隆来获得所有权,把&String转为String

克隆虽然比直接存储引用消耗了更多时间和内存,但它省去了处理生命周期的麻烦,让代码更加直接简单。在某些场景中,放弃一些性能来获取更多的简洁性是非常值得考虑的

当然,使用String::from函数来封装也是可以的:

rust 复制代码
fn parse_config(args: &[String]) -> Config {  
    let query = &args[1];  
    let filename = &args[2];  
    Config {  
        query: String::from(query),  
        filename: String::from(filename),  
    }  
}

当然可行的代码可能不止这两种,这里我就采用第一种克隆的方法。

12.3.5. 把函数变为结构体的方法

既然parse_config会创建一个Config的实例,也就是说它是一个构造函数。对于构造函数,可以这么写:

rust 复制代码
impl Config {  
    fn new(args: &[String]) -> Config {  
        let query = args[1].clone();  
        let filename = args[2].clone();  
        Config {  
            query,  
            filename,  
        }  
    }  
}

只需要把这个函数写在Config的方法上即可(对于方法的详细解释,详见 5.3. struct的方法(Method))。这里还给parse_config改了个名叫new,是因为我把它当作了一个构造函数来处理(构造函数一般都命名为new)。

这么改,main函数里面也需要改一下:

rust 复制代码
let config = Config::new(&args);

12.3.5. 整体代码

以下是截止到本篇文章所写出的所有代码:

rust 复制代码
use std::env;  
use std::fs;  
  
struct Config {  
    query: String,  
    filename: String,  
}  
  
fn main() {  
    let args:Vec<String> = env::args().collect();  
    let config = Config::new(&args);  
  
    let contents = fs::read_to_string(config.filename)  
        .expect("Somthing went wrong while reading the file");  
    println!("With text:\n{}", contents);  
}  
  
impl Config {  
    fn new(args: &[String]) -> Config {  
        let query = args[1].clone();  
        let filename = args[2].clone();  
        Config {  
            query,  
            filename,  
        }  
    }  
}
相关推荐
八了个戒2 小时前
「数据可视化 D3系列」入门第三章:深入理解 Update-Enter-Exit 模式
开发语言·前端·javascript·数据可视化
失去妙妙屋的米奇2 小时前
matplotlib数据展示
开发语言·图像处理·python·计算机视觉·matplotlib
夏天的阳光吖2 小时前
C++蓝桥杯实训篇(四)
开发语言·c++·蓝桥杯
angushine3 小时前
Gateway获取下游最终响应码
java·开发语言·gateway
来自星星的坤3 小时前
SpringBoot 与 Vue3 实现前后端互联全解析
后端·ajax·前端框架·vue·springboot
西贝爱学习3 小时前
数据结构:C语言版严蔚敏和解析介绍,附pdf
c语言·开发语言·数据结构
AUGENSTERN_dc3 小时前
RaabitMQ 快速入门
java·后端·rabbitmq
程丞Q香3 小时前
python——学生管理系统
开发语言·python·pycharm
晓纪同学3 小时前
C++ Primer (第五版)-第十三章 拷贝控制
java·开发语言·c++
烛阴4 小时前
零基础必看!Express 项目 .env 配置,开发、测试、生产环境轻松搞定!
javascript·后端·express