7个版本

0.2.3 2024年6月8日
0.2.2 2023年9月23日
0.1.2 2023年3月27日

#270 in Rust模式

MIT/Apache

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()
}

结果,我们减少了代码量,删除了不必要的重复,并仅保留了必要的代码。我们还为代码重构打开了大门(我们将在以下章节中讨论)

  1. 我们现在可以在不同的文件中分离结构体构建,而无需将它们拖入一个文件;例如,我们可以分别构建配置、数据库工作、支付服务客户端、Kafka连接等。
  2. 我们可以按任何顺序组装它们,而不仅仅是初始化顺序,这意味着我们不必跟踪首先初始化了什么。
  3. 我们可以处理循环依赖。

测试依赖关系

库在运行时解析依赖关系,否则将无法实现循环依赖和任意初始化顺序等特性。这意味着依赖解析需要某种方式进行检查,为此,应该添加一个测试。这很简单。要这么做,只需要调用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