#di #injection #dependencies

mydi_macros

MyDI库的宏

7个版本

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

配置类别中排名644


用于mydi

MIT/Apache

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

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

  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 类型。

它简单地将它添加到注入方法中即可应用

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