在写 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 的语言特性,提出了从纯函数设计到依赖管理的系统性方案。文章逻辑清晰,对初学者和进阶开发者均具有参考价值,以下从多个维度进行分析与建议:
优点与核心理念
纯函数的实践价值
通过对比
foo(input: usize)
与数据库查询函数的差异,准确指出了纯函数的确定性优势。尤其强调了 "输出只依赖输入" 的特性对可预测性与测试性的提升,这一观点符合函数式编程的核心原则,且通过 Rust 的简洁示例降低了理解门槛。依赖注入的结构化抽象
将
DatabaseConnection
作为参数传递的解决方案,有效解决了外部依赖导致的不可控性。进一步通过结构体Foo
封装多个依赖项的设计,既避免了函数参数膨胀,又保持了依赖的显式性,体现了 "依赖明确化" 的现代设计哲学。特别是通过Foo::query(&foo, 0)
的脱糖解释,揭示了方法调用的本质,这对理解 Rust 的self
机制有启发意义。测试环境的适配性
明确指出依赖注入便于替换测试数据库的特性,呼应了 "可测试性是可维护性的基石" 的理念。这一观点在实际开发中尤为重要,尤其适用于需要频繁切换环境的集成测试场景。
可改进之处
示例代码的准确性
在数据库查询示例中,代码
let model = query(db);
存在逻辑问题:函数参数id: i32
未被使用,且query(db)
未传入id
。建议修正为let model = db.query(id);
,以体现实际参数传递。此外,结构体Foo
的new()
方法在示例中缺少参数定义,应补充为pub fn new(db: DatabaseConnection, http: HttpClient) -> Self
,否则代码无法编译。复杂场景的延伸讨论
当前内容聚焦于单个结构体的依赖管理,但实际项目中常需处理多层嵌套依赖(如
Foo
依赖Bar
依赖Baz
)。可补充依赖注入容器(如dependency-injection
crate)或模块化设计的实践方案。此外,Rust 的生命周期(lifetime)特性在管理依赖时可能引入复杂性,例如&DatabaseConnection
的借用规则,这部分值得深入探讨。性能与资源管理的权衡
依赖注入可能带来额外的资源开销(如频繁创建
DatabaseConnection
实例)。可讨论如何结合 Rust 的Arc
(原子引用计数)实现依赖共享,或通过连接池管理数据库资源,以平衡可维护性与性能需求。延伸建议
Result
和Option
类型是纯函数设计的重要组成部分。可补充如何通过返回Result<T, E>
将错误处理纳入纯函数范式,例如fn query(&self, id: i32) -> Result<String, DbError>
。mockall
等库模拟依赖行为,或通过环境变量动态切换数据库连接。总结
文章成功将函数式编程思想与 Rust 的结构化设计相结合,为编写可维护代码提供了清晰的路径。通过修正示例细节并扩展复杂场景的讨论,可进一步提升其指导价值。期待作者在后续文章中深入探讨 Rust 特性与现代软件架构的融合实践。
这篇博客以简洁明了的方式探讨了如何编写易维护的Rust代码,特别是通过纯函数和依赖注入来实现这一目标。作者从基础概念出发,用实际例子引导读者理解这些设计原则的重要性及其在实际中的应用。
文章首先介绍了纯函数的概念,并通过简单的数学运算和数据库查询对比,生动地展示了纯函数的优势,如可预测性和可测试性。这种对比帮助读者快速抓住关键点。
接下来,依赖注入部分深入浅出,作者用具体代码示范了如何将依赖作为参数传递,并进一步优化为结构体,从而保持接口的整洁。这不仅提高了代码的模块性和灵活性,也使得测试更加便捷。特别是通过结构体的方法展示,使得复杂的依赖管理变得清晰易懂。
文章还对Rust语法进行了简要分析,对比了糖衣与实际实现,这有助于读者理解语言背后的机制,增强了代码的可维护性和可扩展性。
建议作者在后续内容中加入更多复杂场景下的示例,如处理全局状态或高级主题,以丰富文章内容。总体而言,这篇博客为Rust开发者提供了实用的指南,值得期待作者未来的深入探讨。