Rust:项目结构示例分步说明
介绍
结论
介绍
Actix-web 最近(希望是暂时的)的死亡迫使我重新检查我的一个宠物项目的一些代码,该项目没有完全解耦,我的一些 Diesel 与我的 Actix 混合在一起,所以我将分享 Rust 中的分解过程和阶段。
本书对此进行了详尽的解释,但我会尽量使其更加简洁明了。为了清晰起见,我会省略许多细节。当然,代码本身并没有什么用处,它的唯一目的是尽可能简单地逐步展示整个过程。希望对大家有所帮助。
我还在 GitHub 上上传了一个代码库 ( https://github.com/robertorojasr/rust-split-example ),如果你克隆它,每次提交都会增加一个步骤。与这个例子不同的是,它会运行(虽然没做什么,但确实能运行)。
这种演变应该是这样的:
- 包含所有内容的单个文件。
- 将部分代码移至不同的模块。(此步骤在 repo 中缺失,我忘记了,起诉我吧),请不要起诉我。
- 将模块移动到另一个文件。
- 将单个文件中的模块变成一个文件夹,作为一个模块,其中有多个文件作为子模块。
- 将您的板条箱分成位于同一目录树中的库和可执行文件。
- 将可执行文件和库移动到具有其自己的目录树的不同包(工作区)。
当我们的项目变得更大、更复杂时,这些策略将有助于保持代码的整体结构易于管理、清晰和解耦。
0. 起点
因此,您创建了“Hello World”,一切都很酷,您替换了那个 println 并开始编码,很快您就注意到您可爱的 main.rs 变得越来越大,充满了结构、函数和特征,其中一些相互关联,但有些则不然,就像尼奥看黑客帝国一样,您开始看到一些宏观结构,然后您告诉自己,“是时候解决这个烂摊子了”,当然,您可以在开始之前设计好一切(实际上您应该这样做),但您要避免过度设计,正如这篇文章所示(至少希望如此),没有必要从一开始就采取强硬措施。
可以说这是你目前的杰作,令人印象深刻,我知道,这一切都是我自己完成的。
// src/main.rs
struct A {
a: i32,
}
struct B {
b: i32,
}
fn main() {
let first = A { a: 42, };
}
1. 一个模块
那么你该如何开始呢?第一件事就是制作模块来封装一些代码
// src/main.rs
mod something {
struct A {
a: i32,
}
struct B {
b: i32,
}
}
fn main() {
let first = A { a: 42, };
}
但现在有一个问题,即使A在同一个文件中定义,也不再在同一个模块中,所以main()不知道其中有东西,所以我们必须导入一些东西
// src/main.rs
mod something {
struct A {
a: i32,
}
struct B {
b: i32,
}
}
use crate::something::*; // <- this is new
fn main() {
let first = A { a: 42, };
}
现在main()知道这里有一些东西,并且它可以使用其中的所有公共内容,但是等等,其中没有任何公共内容,所以我们必须将我们想要让main()看到的所有内容公开。
// src/main.rs
mod something {
pub struct A { // <- this is new
pub a: i32, // <- this is new
}
pub struct B {
pub b: i32, // <- this is new
}
}
use crate::something::*;
fn main() {
let first = A { a: 42, };
}
现在main()可以看到里面的所有内容,当然你不必将所有内容都公开,只需公开你需要的内容即可,并且不要用 OOP 参数来表示你不应该公开数据,这只是一个例子,顺便说一下,OOP 并不是全部(这里有点无礼)。
2. 其他文件中的模块
但这并不能解决你的 IDE 因文件仍然很大而变得迟缓的问题,事实上你只是添加了更多的东西!为了解决这个问题你应该开始使用 Vim...我在开玩笑...(是吗?)
因此,现在您想将该模块移到外部,移到其他文件,以便自由且独立地生存。
因此,您将定义和导入保留在main.rs中
// src/main.rs
mod something;
// the content of the module was here
use crate::something::*;
fn main() {
let first = A { a: 42, };
}
并将模块内容放入新创建的文件中,该文件与main.rs位于同一文件夹中
// src/something.rs
pub struct A {
pub a: i32,
}
pub struct B {
pub b: i32,
}
当你在main.rs中添加模块时
// src/main.rs
mod something;
// the rest of it
Rust 会自动在文件中查找它,如果找不到,则在同一文件夹(在本例中为 src/)中查找具有模块名称的文件,如果仍然找不到,则查找具有模块名称和文件mod.rs 的文件夹,然后在那里查找代码。
3. 文件夹中的模块包含多个子模块
正如上一段提到的,我们可以进一步拆分我们的模块,
我们所要做的就是创建一个文件夹,这样我们就可以得到:
src/
|_ main.rs
|_ something.rs
|_ something/
现在,我们可以将something.rs重命名为mod.rs,并将其移到something/中,但这样做有什么意义呢?我们想要拆分东西!所以我们要给A和B分别赋予它们自己的模块(请不要在类似的例子之外这样命名你的东西)。这样我们就有了这个树。
src/
|_ main.rs
|_ something/
|_ mod.rs
|_ a.rs
|_ b.rs
但是对于something.rs会发生什么呢,好吧我的朋友,你把它分开,A转到a.rs并且你可以猜出B去了哪里。
现在A和B都在它们自己的模块中,因此我们相应地修改导入
// src/main.rs
use crate::something::a::*; // <- this is new
use crate::something::b::*; // <- this is new
fn main() {
let first = A { a: 42, };
}
但是mod.rs现在有责任调用它们的孩子,正如我之前所说的,当 Rust 检查something.rs并且没有找到时,它会检查文件夹something,然后在里面寻找名为mod.rs的文件。
// src/something/mod.rs
pub mod a;
pub mod b;
你可能会注意到,这和我们一开始对 main 函数做的事情是一样的。你可以根据需要不断嵌套模块,就像这样。
// src/something/a.rs
pub struct A {
pub a: i32,
}
// src/something/b.rs
pub struct B {
pub b: i32,
}
很好,但是现在怎么办?如果我的项目很庞大,如果我可以重用部分代码怎么办?你之前有一个很棒的 Web 应用,使用了 Diesel ORM 和 Actix-web,但万一哪天 Actix 的创始人离职了,项目的未来变得不确定,那该怎么办?那该怎么办?!
...
好吧,您可以将您的 webapp 视为一个处理 DB 的库,比如说使用 Diesel 和 Actix-web 或其他框架的可执行文件中的单独消费者,这些框架显然没有消失,但可能会回来......
例如,您还可以创建一个使用相同 DB 相关代码库的 CLI UI。我们开始吧!
4. 带有可执行文件的 Rust 库
您可能已经阅读并跳过了更多有趣的内容,Rust 识别两种类型的板条箱(我一直称之为项目的正式名称,只是因为我是一个反叛者)库和可执行文件,您可能知道它们之间的区别,但为了完整起见,让我们简单一点,可执行文件是您直接使用的东西,而库是可执行文件使用的东西。
在这种情况下,我们想要将main()作为可执行文件放入另一个文件中,并将所有结构留在库中,以便将来在其他可执行文件中重用。
Rust 非常努力地让事情变得简单(因为它对所有与洞穴和生命周期相关的痛苦感到内疚),所以要将一个板条箱变成一个库,你只需将文件main.rs重命名为lib.rs,瞧,现在是一个库,但让我们用它做一些有用的东西,要做到这一点,我们将创建一个新的文件夹bin/,我们将复制我们现有的main.rs,(现在重命名为lib.rs)在上面,用一些花哨的描述性名称,给你留下这个新树:
src/
|_ lib.rs // <- just a renamed main.rs
|_ bin/ // <- this folder is new
|_ |_ framework_that_broke_my_heart.rs // <- a copy of our ex-main.rs
|_ something/
|_ mod.rs
|_ a.rs
|_ b.rs
所以,something/里面的所有内容现在都不会被触动,我们先来处理framework_that_broke_my_heart.rs。首先要注意的是,bin/里面的所有内容都处于一个泡沫宇宙中,即使我们 crate 里面的内容也不再是它的一部分了,就像我们和社会一样(不是你,适应良好的程序员……呃),所以我们必须调用我们新创建的库(你知道,当我们把main.rs重命名为lib.rs时),就像我们调用任何库一样。
// src/bin/framework_that_broke_my_heart.rs
extern crate this_example; // oh right, I never named
// this crate, is the name you give in Cargo.toml
// under [package] in tha *name* field
// (don't use dashes on it)
use crate::something::a::*;
use crate::something::b::*;
fn main() {
let first = A { a: 42, };
}
并在lib.rs ex-main.rs中
// src/lib.rs
pub mod something; // <- this is all, is like telling Rust
// copy/paste everything inside `something` inside a `mod` here
物体内部的所有东西都未受影响。
我们已经接近终点线了,现在你可能想知道那个可怜的家伙的情况,他最喜欢的框架的早期消亡让他心碎,并告诉自己,他到底是怎么把所有的 Actix-web 代码放在一个很小的文件中,那个文件一定很大,很乱,但整个想法是分裂事物,他只是让一切变得更糟!好吧,伙计们,这就是下一个要点。
5. 工作区
这会将你放在一个类似大箱子的保护伞下的几个较小的箱子里,你现在可以将代码拆分成两个箱子了,毕竟,我们声明了后续步骤中的可执行文件已经不属于原始箱子了,没错。将代码完全拆分成两个独立的箱子是一个有效的选择,但许多依赖项对可执行文件和库都是通用的;而且重建两次、测试两次等等会很麻烦。如果库和可执行文件彼此相关,你可能希望将它们视为一个整体,以便进行构建/测试/运行,你也可以将它们保存在同一个仓库中。
这会变得有点复杂,但不会太复杂。
我们将在原来的箱子里制作两个箱子,然后将它们粘在一起。
于是我们就有了这样的美丽:
./
|_Cargo.toml
|_Cargo.lock
|_target/
| |_ ... // we don't care about this, is made in the building process
|
|_src/
|_ lib.rs
|_ bin/
|_ |_ framework_that_broke_my_heart.rs
|_ something/
|_ mod.rs
|_ a.rs
|_ b.rs
坐在“货物”旁边,我们刚刚制作了两个新板条箱。
$ cargo init --lib db_stuff
$ cargo init ftbmh // framework_that_broke_my_heart, too long,
// too lazy, again, this is an example, name your thing with
// common sense, don't be // funny in a real project, the fun
// will last about 10min, the pain much // more than that.
参数 --lib 唯一的作用是,改为生成main.rs并生成lib.rs,默认情况下生成可执行文件。
因此我们将得到:
./
|_Cargo.toml
|_Cargo.lock
|_target/
| |_ ...
|
|_src/
| |_ lib.rs
| |_ bin/
| |_ |_ framework_that_broke_my_heart.rs
| |_ something/
| |_ mod.rs
| |_ a.rs
| |_ b.rs
|
| // ^ that's the old part
|
|_db_stuff/ // this whole folder is new
| |_Cargo.toml
| |_src/
| |_ lib.rs // <- that's all the --lib does
|_ftbmh/ // this whole folder is new
|_Cargo.toml
|_src/
|_ main.rs
现在,我们将从原始的Cargo.toml中根据需要将部分内容移动到新的Cargo.toml中,例如将依赖项移动到需要它们的任何人。
完成后,只需清理旧的Cargo.toml并输入以下内容:
[workspace]
members = ["db_stuff", "ftbmh"]
就是这样,现在原始的Cargo.toml没有 [package] 或 [dependencies] 部分;原始板条箱现在是一个外壳。
当您在里面制作 2 个板条箱(dn_stuff和ftbmh)时,cargo 看到您位于一个现有的板条箱内,它有自己的 git 仓库,所以没有为它们制作一个,您的旧仓库仍然良好且健康。
现在请记住,您拆分了代码,并且可能一部分依赖于其他部分,在这种情况下,ftbmh依赖于db_stuff,因此我们必须在ftbmh Cargo.toml文件中添加该依赖项
// ftbmh/Cargo.tom
[package]
// your stuff
[dependencies]
// your dependencies
db_stuff = { path = "../db_stuff" } // as you may know the `..`
// in the path refers to the mother folder of the current one
您可能还记得上一步中ftbmh/main.rs已经在原始板条箱之外,因此一切都在那里完成,并且板条箱 db_stuff 已经被用作外部板条箱,所以那里的一切也都是一样的。
结论
我们完成了。最初只是一个简单的单文件项目,现在变成了一个包含 2 个工作区的复杂 crate,当然,工作区可能是 3 个,甚至可能是 100 个,只需重复上述步骤即可,Rust 命名空间中的模块也同样如此。
如你所见,如果你的代码正确解耦,整个过程会非常低调。当然,从一开始就规划好设计是件好事,但有时项目的发展会超出我们的预期,变得更加复杂,有方法轻松地进行调整而不造成混乱是件好事,也因为你不必为了担心未来可能出现的问题(例如本文中提到的情况)而过度设计你的解决方案。你可以有机地扩展你的代码。
希望以上内容对大家有所帮助。虽然这没什么新意,但我发现文档和书籍中的内容太过分散。相关内容还有很多,但我并没有过多讨论公共/私有属性以及默认哪些属性对哪些人可见,这些在本书和精彩的O'Reilly 的《Rust 编程》中都有很好的解释。我只是尝试搭建一个框架,方便以后轻松挂载细节。
任何更正和建议,请随时告诉我,特别是如果写得很奇怪,英语不是我的母语,这是我目前最好的。
鏂囩珷鏉ユ簮锛�https://dev.to/ghost/rust-project-struct-example-step-by-step-3ee