7个版本
0.2.3 | 2024年6月8日 |
---|---|
0.2.2 | 2023年9月23日 |
0.1.2 | 2023年3月27日 |
在配置类别中排名644
用于mydi
36KB
299 行
My DI / 依赖注入库
简短描述和主要功能
一个专注于简单性和组合性的Rust依赖注入(DI)库。主要功能包括
- 使用宏的简单设计
- 支持循环依赖
- 支持任意初始化顺序
- 与dyn特质一起工作
- 通过标记使用相同类型的多个结构
- 使用默认特质和任意函数作为默认参数
- 不仅能够组装类,还可以将类拆分为组件。例如,用于配置。
此库通过组织各种应用程序组件(如配置、数据库连接、支付服务客户端、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 类型。
它简单地将它添加到注入方法中即可应用
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特质一起工作
在某些情况下,从类型中抽象出来并与 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
一起工作?一个原因是为了从实现中抽象出来并简化mock的使用,例如来自mockall库的mock。
但是,如果你需要使用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()
方法自动为T类型创建Arc和Box容器的实现。
#[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!()
}
您还可以使用标记。为此,有一个特殊的标记结构,允许您用标记包装结构。例如:
// 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()?;
}
标记类型实现了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 2.0 或 MIT 许可证下授权。
贡献
欢迎任何贡献。只需编写测试并提交合并请求即可
路线图
- 更好地处理默认值
- 添加 Cargo 功能
- 添加 ahash 支持
- 自定义错误
特别感谢
- Chat GPT-4,它帮助我编写所有这些文档并纠正代码中的大量错误
- Kristina,她是我的灵感来源
- 我作为参考使用的 Java、Scala 和 Rust 中的许多库
- 库作者,你们是最棒的
- Stable Diffusion,它帮助我创建标志 :-)
相关项目
rust
java
scala
依赖项
~270–720KB
~17K SLoC