7个版本
0.2.3 | 2024年6月8日 |
---|---|
0.2.2 | 2023年9月23日 |
0.1.2 | 2023年3月27日 |
#270 in Rust模式
52KB
705 行
My DI / 依赖注入库
简要描述和主要功能
A Rust Dependency Injection (DI) library focused on simplicity and composability. Key features include
- 使用宏的简单设计
- 支持循环依赖
- 支持任意初始化顺序
- 与dyn traits协同工作
- 通过标记使用相同类型的多结构
- 使用默认特性和任意函数作为默认参数
- 不仅能够组装类,还可以将类解构为组件。例如,用于配置。
该库通过组织各种应用组件(如配置、数据库连接、支付服务客户端、Kafka连接等)的组装和集成,简化了具有许多嵌套结构的复杂项目的管理。虽然不直接提供这些组件,但如果应用程序由这些元素组成,该库将大大简化应用程序结构的组织和管理工作。My DI确保依赖关系管理保持有序、易于阅读和可扩展,为项目的增长奠定坚实的基础。
如何连接库?
只需将依赖项添加到Cargo.toml文件中
[dependencies]
mydi = "0.1.2"
那么,问题是什么?为什么我需要这个?
在Java和Scala等语言中,使用独立机制进行DI的方法很常见,但在Rust中并不常见。为了了解这个库的需求,让我们看看一个没有My DI的例子和一个有My DI的例子。让我们用纯Rust构建几个结构(Rust程序有时由数百个嵌套结构组成)。
问题!
struct A {
x: u32
}
impl A {
pub fn new(x: u32) -> Self {
Self { x }
}
}
struct B {
x: u64
}
impl B {
pub fn new(x: u64) -> Self {
Self { x }
}
}
struct C {
x: f32
}
impl C {
pub fn new(x: f32) -> Self {
Self { x }
}
}
struct D {
a: A,
b: B,
c: C
}
impl D {
pub fn new(a: A,
b: B,
c: C) -> Self {
Self { a, b, c }
}
pub fn run(self) {
todo!()
}
}
fn main() {
let a = A::new(1);
let b = B::new(2);
let c = C::new(3.0f32);
let d = D::new(a, b, c);
d.run()
}
如您所见,我们至少在每个结构体声明中写入4个参数
- 在结构体声明中,
- 在构造函数参数中,
- 在构造函数中的结构体字段中,
- 然后也在构造函数中替换参数。随着项目的增长,所有这些都将变得更加复杂和混乱。
解决方案!
现在让我们尝试使用My DI简化所有这些
use mydi::{InjectionBinder, Component};
#[derive(Component, Clone)]
struct A {
x: u32
}
#[derive(Component, Clone)]
struct B {
x: u64
}
#[derive(Component, Clone)]
struct C {
x: f32
}
#[derive(Component, Clone)]
struct D {
a: A,
b: B,
c: C
}
impl D {
pub fn run(self) {
todo!()
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let injector = InjectionBinder::new()
.instance(1u32)
.instance(2u64)
.instance(3f32)
.inject::<A>()
.inject::<B>()
.inject::<C>()
.inject::<D>()
.build()?;
let d: D = injector.get()?;
d.run()
}
结果,我们减少了代码量,删除了不必要的重复,并仅保留了必要的代码。我们还为代码重构打开了大门(我们将在以下章节中讨论)
- 我们现在可以在不同的文件中分离结构体构建,而无需将它们拖入一个文件;例如,我们可以分别构建配置、数据库工作、支付服务客户端、Kafka连接等。
- 我们可以按任何顺序组装它们,而不仅仅是初始化顺序,这意味着我们不必跟踪首先初始化了什么。
- 我们可以处理循环依赖。
测试依赖关系
库在运行时解析依赖关系,否则将无法实现循环依赖和任意初始化顺序等特性。这意味着依赖解析需要某种方式进行检查,为此,应该添加一个测试。这很简单。要这么做,只需要调用verify方法。通常,在最终依赖关系组装后调用它就足够了。例如,像这样
use mydi::{InjectionBinder, Component};
fn build_dependencies(config: MyConfig) -> InjectionBinder<()> {
todo!()
}
#[cfg(test)]
mod test_dependencies_building {
use std::any::TypeId;
use sea_orm::DatabaseConnection;
use crate::{build_dependencies, config};
use std::collections::HashSet;
#[test]
fn test_dependencies() {
let cfg_path = "./app_config.yml";
let app_config = config::parse_config(&cfg_path).unwrap();
let modules = build_dependencies(app_config);
let initial_types = HashSet::from([ // types that will be resolved somewhere separately, but for the purposes of the test, we add them additionally
TypeId::of::<DatabaseConnection>(),
TypeId::of::<reqwest::Client>()
]);
// the argument true means that in the errors, we will display not the full names of the structures, but only the final ones
// if you are interested in the full ones, you should pass false instead
modules.verify(initial_types, true).unwrap();
}
}
模块化架构和组合
文件和文件夹的组织
如何组织具有许多依赖关系的项目?这可能会根据你的喜好而有所不同,但我的偏好如下文件夹结构
- main.rs
- modules
-- mod.rs
-- dao.rs
-- clients.rs
-- configs.rs
-- controllers.rs
-- services.rs
-- ...
这意味着有一个单独的文件夹用于组装依赖项,每个文件夹都负责一组特定的服务。或者,如果你更喜欢,可以按领域划分服务,而不是按功能目的
- main.rs
- modules
-- mod.rs
-- users.rs
-- payments.rs
-- metrics.rs
-- ...
两种选择都是正确的,并且可以正常工作,具体使用哪一种更多的是个人喜好的问题。在每个模块中,将组装自己的InjectionBinder
,在main.rs中会有类似以下的内容
use mydi::{InjectionBinder, Component};
#[derive(Component, Clone)]
struct MyApp {}
impl MyApp {
fn run(&self) {
todo!()
}
}
fn merge_dependencies() -> Result<InjectionBinder<()>, Box<dyn std::error::Error>> {
let result = modules::dao::build_dependencies()
.merge(modules::configs::build_dependencies()?) // Of course, during dependency assembly, something might fail
.merge(modules::clients::build_dependencies())
.merge(modules::services::build_dependencies())
.merge(modules::controllers::build_dependencies())
.inject::<MyApp>();
Ok(result)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let injector = merge_dependencies()?.build()?;
let app: MyApp = injector.get()?;
app.run();
Ok(())
}
单独模块的组织
所以,模块本身将如何看起来?这也许也会根据个人喜好而有所不同。我更喜欢使用配置作为特定的实例。
use mydi::InjectionBinder;
pub fn build_dependencies(app_config_path: &str,
kafka_config_path: &str) -> Result<InjectionBinder<()>> {
let app_config = AppConfig::parse(app_config_path)?;
let kafka_config = KafkaConfig::parse(kafka_config_path)?;
let result = InjectionBinder::new()
.instance(app_config)
.instance(kafka_config)
// ...
.void();
Ok(result)
}
同时,控制器的模块可以组装如下
use mydi::{InjectionBinder, Component};
pub fn build_dependencies() -> InjectionBinder<()> {
InjectionBinder::new()
.inject::<UsersController>()
.inject::<PaymentsController>()
.inject::<OrdersController>()
// ...
.void()
}
注意结尾的.void()
。在将每个组件添加到InjectionBinder
后,它将内部类型转换为传入的类型。因此,为了简化类型的工作,将其转换为()
类型是有意义的,这就是.void()
方法的作用。
使用宏添加依赖项
要添加依赖项,最好的方法是使用derive宏Component
use mydi::{InjectionBinder, Component};
#[derive(Component, Clone)]
struct A {
x: u32,
y: u16,
z: u8,
}
它将生成必要的ComponentMeta
宏,然后你可以通过inject方法添加依赖项
fn main() -> Result<(), Box<dyn std::error::Error>> {
let injector = InjectionBinder::new()
.instance(1u32)
.instance(2u16)
.instance(3u8)
.inject::<A>()
.build()?;
todo!()
}
使用函数添加依赖项
在某些情况下,使用宏可能不太方便,因此使用函数代替是有意义的。为此,使用inject_fn
方法
use mydi::{InjectionBinder, Component};
#[derive(Component, Clone)]
struct A {
x: u32,
}
#[derive(Component, Clone)]
struct B {
a: A,
x: u32,
}
#[derive(Clone)]
struct C {
b: B,
a: A,
x: u64,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let inject = InjectionBinder::new()
.inject_fn(|(b, a, x)| C { b, a, x })
.inject::<B>()
.inject::<A>()
.instance(1u32)
.instance(2u64)
.build()?;
let x = inject.get::<C>()?;
}
请注意参数中的括号。这里的参数接受一个元组。因此,对于0个参数,你需要这样写参数|()|
,对于单个参数,你需要这样写元组|(x, )|
。
默认参数
要添加默认值,可以使用指令 #[component(...)]
。目前,只有两种可用的选项:#[component(default)]
和 #[component(default = my_func)]
,其中 my_func 是作用域内的一个函数。 #[component(default)]
将值替换为 Default::default()
。例如,像这样
#[derive(Component, Clone)]
struct A {
#[component(default)]
x: u32,
#[component(default = custom_default)]
y: u16,
z: u8,
}
fn custom_default() -> u16 {
todo!()
}
请注意,custom_default 是不带括号 ()
被调用的。另外,目前不支持嵌套模块的调用,这意味着 foo::bar::custom_default
将不会工作。为了解决这个问题,只需使用 use
将函数调用引入作用域。
如何读取值?
由于依赖关系组装,将创建一个注入器,您可以从其中获取依赖项本身。目前,有两种获取值的方式:获取单个依赖项和获取元组。
use mydi::{InjectionBinder, Component};
#[derive(Component, Clone)]
struct A {}
#[derive(Component, Clone)]
struct B {}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let injector = InjectionBinder::new()
.inject::<A>()
.inject::<B>()
.build();
let a: A = injector.get()?; // getting a single value
let (a, b): (A, B) = injector.get_tuple()?; // getting a tuple
todo!()
}
目前支持最多 18 维度的元组。
泛型
宏中的泛型也得到支持,但限制是它们必须实现 Clone
特性和具有 'static
生命周期
use mydi::{InjectionBinder, Component};
#[derive(Component, Clone)]
struct A<T: Clone + 'static> {
x: u32
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let injector = InjectionBinder::new()
.instance(1u32)
.instance(2u64)
.inject::<A<u32>>()
.inject::<A<u64>>()
.build()?;
let a: A = injector.get::<A<u32>>()?;
todo!()
}
循环依赖
在某些复杂情况下,需要组装循环依赖。在典型情况下,这会导致异常和构建错误。但针对这种情况,有一个特殊的 Lazy 类型。
只需将其添加到 inject 方法中即可应用它
use mydi::{InjectionBinder, Component, Lazy};
#[derive(Component, Clone)]
struct A {
x: Lazy<B>
}
struct B {
x: A,
y: u32
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let injector = InjectionBinder::new()
.instance(1u32)
.inject::<A>()
.inject::<B>()
.inject::<Lazy<B>>()
.build()?;
let a: A = injector.get::<A>()?;
todo!()
}
此外,值得注意的是,禁止嵌套 lazy 类型
与dyn traits协同工作
在某些情况下,从类型抽象出来并与 Arc 或 Box 一起工作是有意义的。对于这些情况,有一个特殊的 auto 特性和 erase! 宏。
例如,像这样
use mydi::{InjectionBinder, Component, erase};
#[derive(Component, Clone)]
pub struct A {
x: u32,
}
trait Test {
fn x(&self) -> u32;
}
impl Test for A {
fn x(&self) -> u32 {
self.x
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let inject_res = InjectionBinder::new()
.inject::<A>().auto(erase!(Arc<dyn Test>))
.instance(1u32)
.build()?;
let dyn_type = inject_res.get::<Arc<dyn Test>>()?;
}
这里发生了什么? auto
只是基于前面的类型添加一个新依赖项,而不将其添加到 InjectionBinder 的类型中。换句话说,您可以通过编写 .inject_fn(|(x, )| -> Arc<dyn Test> { Arc::new(x) })
来实现相同的效果,但这将需要编写大量的样板代码,这是您想要避免的。
我们为什么需要与 dyn traits
合作?一个原因是将实现细节抽象化,简化使用模拟,例如来自 mockall 库的模拟。
但如果你需要使用 Box
而不是 Arc
,你需要使用库 dyn-clone
use mydi::{InjectionBinder, Component};
use dyn_clone::DynClone;
#[derive(Component, Clone)]
pub struct A {
x: u32,
}
trait Test: DynClone {
fn x(&self) -> u32;
}
dyn_clone::clone_trait_object!(Test);
impl Test for A {
fn x(&self) -> u32 {
self.x
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let inject_res = InjectionBinder::new()
.inject::<A>().auto(erase!(Box<dyn Test>))
.instance(1u32)
.build()?;
let dyn_type = inject_res.get::<Box<dyn Test>>()?;
}
自动装箱
由于我们在 InjectionBinder 中存储类型信息,我们可以使用 .auto_arc() 和 .auto_box() 方法自动为 Arc 和 Box 容器创建类型 T 的实现。
#[derive(Component, Clone)]
struct MyStruct {}
#[derive(Component, Clone)]
struct MyNestedStruct {
my_struct_box: Box<MyStruct>,
my_struct_arc: Arc<MyStruct>,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let inject_res = InjectionBinder::new()
.inject::<MyStruct>().auto_box().auto_arc()
.inject::<MyNestedStruct>()
.build()?;
}
此外,如果存在 Component 注解,则 Arc<...> 内的类型可以直接传递给 inject 方法。例如,如下所示:.inject<Box<MyStruct>>
重要的是要注意,原始类型仍然可用,并且不会被删除。
重复依赖和标记
在某些情况下,有必要使用相同类型的多个实例,但默认情况下,如果传递了两个相同类型的实例,则汇编将失败并出现错误。但是,有时这可能是必要的,例如,当连接到多个 Kafka 集群、使用多个数据库等。为此,可以使用泛型或标记。
泛型使用示例
#[derive(Component, Clone)]
struct MyService<KafkaConfig: Clone + 'static> {
config: KafkaConfig
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config1: Config1 = todo!();
let config2: Config2 = todo!();
let inject_res = InjectionBinder::new()
.inject::<MyService<Config1>>()
.inject::<MyService<Config2>>()
.instance(config1)
.instance(config2)
.build()?;
todo!()
}
您还可以使用标记。为此,有一个特殊的 Tagged 结构,允许您在标记中包装结构。例如,如下所示
// This type will be added to other structures
#[derive(Component, Clone)]
struct MyKafkaClient {}
// These are tags, they do not need to be created, the main thing is that there is information about them in the type
struct Tag1;
struct Tag2;
#[derive(Component, Clone)]
struct Service1 {
kafka_client: Tagged<MyKafkaClient, Tag1>
}
#[derive(Component, Clone)]
struct Service2 {
kafka_client: Tagged<MyKafkaClient, Tag2>
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client1: Tagged<MyKafkaClient, Tag1> = {
let client1 = todo!();
Tagged::new(client1)
};
let client2: Tagged<MyKafkaClient, Tag2> = {
let client2 = todo!();
Tagged::new(client2)
};
let inject_res = InjectionBinder::new()
.inject::<Service1>()
.inject::<Service2>()
.instance(client1)
.instance(client2)
.build()?;
}
Tagged 类型实现了 std::ops::Deref,这使得您可以通过它直接调用嵌套对象的函数。
扩展
基本扩展
不仅可以组装类,还可以将它们分解为组件。这在处理配置结构的情况下非常有用。例如,如果我们有一个对象树,我们可以自动注入嵌套结构字段的对象。
#[derive(Clone, mydi::ComponentExpander)]
struct ApplicationConfig {
http_сonfig: HttpConfig,
cache_сonfig: CacheConfig
}
#[derive(Clone)]
struct HttpConfig {
port: u32,
host: String
}
#[derive(Clone)]
struct CacheConfig {
ttl: std::time::Duration,
size: usize
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config1: ApplicationConfig = todo!();
let inject_res = InjectionBinder::new()
.expand(config1)
// .instance(config1.http_сonfig) these two substitutions will be done inside expand
// .instance(config1.cache_сonfig)
.build()?;
todo!()
}
扩展过程中的字段忽略
在某些情况下,我们想要注入的不仅仅是所有字段,而是其中的一些。对于这些场景,请使用指令 #[ignore_expand]
#[derive(Clone, mydi::ComponentExpander)]
struct ApplicationConfig {
http_сonfig: HttpConfig,
#[ignore_expand] // this field will now not be injected
cache_сonfig: CacheConfig
}
#[derive(Clone)]
struct HttpConfig {
port: u32,
host: String
}
#[derive(Clone)]
struct CacheConfig {
ttl: std::time::Duration,
size: usize
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config1: ApplicationConfig = todo!();
let inject_res = InjectionBinder::new()
.expand(config1)
// .instance(config1.http_сonfig) this substitution will be done inside expand
.build()?;
todo!()
}
嵌套扩展
您还可以扩展嵌套结构。为此,请使用注解 #[nested_expand]
。需要注意的是,结构本身不会被扩展。但是,如果您需要扩展它,可以使用 #[force_expand]
注解。
#[derive(Clone, mydi::ComponentExpander)]
struct ApplicationConfig {
http_сonfig: HttpConfig,
#[nested_expand] /// the fields of this structure will be injected, but the structure itself won't be
// #[force_expand] if you uncomment this annotation, this field will also be injected
logic_config: BusinessLogicConfigs
}
#[derive(Clone, mydi::ComponentExpander)]
struct NestedExpanding {
port: u32,
host: String
}
#[derive(Clone, mydi::ComponentExpander)]
struct BusinessLogicConfigs {
some_logic: LogicConfigs // this structure will be injected
}
#[derive(Clone)]
struct LogicConfigs {
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config1: ApplicationConfig = todo!();
let inject_res = InjectionBinder::new()
.expand(config1)
// .instance(config1.http_сonfig) this substitution will be done inside expand
.build()?;
todo!()
}
限制
当前实现限制
- 所有类型都必须是 'static 并实现 Clone
- 使用大量堆,因此不支持 no_std
- 值得注意的是,在构建依赖关系时可能会创建多个副本,这对于大多数长期运行的应用程序来说可能不是关键问题,并且根据基本测试,它的性能比简单的配置解析快 1-2 个数量级。
许可
根据您的选择,在 Apache License,Version 2.0 或 MIT 许可下许可。
贡献
任何贡献都受欢迎。只需编写测试并提交合并请求即可
路线图
- 更好地处理默认值
- 添加 Cargo 功能
- 添加 ahash 支持
- 自定义错误
特别感谢
- Chat GPT-4,它帮助我编写所有这些文档并在代码中纠正了大量错误
- Kristina,她是我的灵感来源
- 我用作参考的Java、Scala和Rust中的众多库
- 库作者,你们是最棒的
- 稳定扩散,它帮助我创建了这个标志 :-)
相关项目
rust
java
scala
依赖项
~0.8–6MB
~32K SLoC