在写 Rust 的时候尝试写出易维护的代码,其中大量参考了函数式的思想。
纯函数
理想情况下应该使用纯函数,因为纯函数输出只依赖于输入,不会因为其他外部因素而改变。
fn foo(input: usize) -> usize {
input * 2
}
在上面的函数中会将输入参数乘以 2 再输出,它的输出是确定且显而易见的,如果输出了 2,则一定只会输出 4,不可能会出现其他输出。这样的纯函数非常好维护。
来看一个复杂一点的函数,它会查询数据库:
fn query(id: i32) -> String {
let db = DatabaseConnection::new();
let model = query(db);
model.name
}
上面的函数通过 id 查询数据库离某条数据的 name,它不是一个纯函数,因为它完全依赖了一个外部的数据库。假设它接收了参数 0
,查询数据库得到 name
为 name_1
,但是在下次接收参数 0
,得到的可能就是 name_233
了。因为数据库的数据可能已经发生了变化。
且这样的依赖也可能让环境的配置变得困难。想象一下我想要在测试环境测试 query
,但是它里面使用了 DatabaseConnection::new()
,我要怎么让它在测试环境下使用测试数据库呢?
依赖注入
一种方式就是将需要的依赖当作入参传递给函数。
fn foo(id: i32, db: DatabaseConnection) -> String {
...
}
像上面那样,不在函数里去获取函数外的依赖 DatabaseConnection
,而是将依赖作为入参传递,这样依赖,函数的执行结果不再受到入参以外的因素影响。如果需要在测试环境中使用测试数据库,也只不过改变传入的 db
参数而已。
Struct 的依赖
上面的例子的函数里有两个入参,如果函数需要的依赖多了全放在入参里就会很难看。我们就可以把所有的依赖抽象成一个 struct。
struct Foo {
db: DatabaseConnection,
http: HttpClient,
...
}
impl Foo {
pub fn new(db: DatabaseConnection, http: HttpClient) -> Self {
...
}
pub fn query(&self, id: i32) -> String {
...
}
}
上面的 struct 中就包含了外部依赖,假设是 DatabaseConnection
和 HttpClient
等十几个吧,我们还贴心提供了 new()
方法用于初始化这个 struct。将依赖放在 struct 中后,调用查询的方法 query
就会变得非常清爽——只需要一个 id
参数。
虽然这样的 query
严格上来说是一个方法而不是函数了,但它和纯函数仍然是类似的。我们先把他看作是一个函数,你会看到方法体内会有 self.db
、self.http
之类的写法,看起来好像是引用了这个函数外部的依赖,它的写法会像是:
let foo = Foo::new();
let name = foo.query(0);
它好像隐藏了它的 struct 的细节,但如果我们换一种写法,就会发现它其实还是一个纯函数:
let foo = Foo::new();
let name = Foo::query(&foo, 0);
上面是脱糖的写法,脱糖后能够看到第一种写法是隐藏了一个 self
指针,函数需要的依赖仍然是通过入参的方式传递给 query
函数使用。
这样的方式可以很轻松得得到一个易于维护的对象,它的行为十分好预测,当出现错误时也很好定位到问题。如果需要对 Foo 进行测试,也只不过是在构建 Foo 时候给它一个测试用的依赖。
这篇博客以简洁明了的方式探讨了如何编写易维护的Rust代码,特别是通过纯函数和依赖注入来实现这一目标。作者从基础概念出发,用实际例子引导读者理解这些设计原则的重要性及其在实际中的应用。
文章首先介绍了纯函数的概念,并通过简单的数学运算和数据库查询对比,生动地展示了纯函数的优势,如可预测性和可测试性。这种对比帮助读者快速抓住关键点。
接下来,依赖注入部分深入浅出,作者用具体代码示范了如何将依赖作为参数传递,并进一步优化为结构体,从而保持接口的整洁。这不仅提高了代码的模块性和灵活性,也使得测试更加便捷。特别是通过结构体的方法展示,使得复杂的依赖管理变得清晰易懂。
文章还对Rust语法进行了简要分析,对比了糖衣与实际实现,这有助于读者理解语言背后的机制,增强了代码的可维护性和可扩展性。
建议作者在后续内容中加入更多复杂场景下的示例,如处理全局状态或高级主题,以丰富文章内容。总体而言,这篇博客为Rust开发者提供了实用的指南,值得期待作者未来的深入探讨。