#inversion #di #ioc #solid #dependencies

nightly he_di

依赖倒置 / 依赖注入 / 控制反转容器 for Rust

4 个版本

使用旧的 Rust 2015

0.2.1 2017年6月29日
0.2.0 2017年6月24日
0.1.1 2017年6月20日
0.1.0 2017年6月20日

#7 in #inversion


用于 he_di_derive

MIT/Apache

63KB
631 代码行

什么是依赖注入(又称依赖倒置)?

控制反转的核心理念是,不是将您的应用程序中的类绑定在一起并让类“new up”它们的依赖项,而是将其颠倒过来,以便在类构造期间传递依赖项。它是 SOLID 编程的 5 个核心原则之一。

如果您想了解更多关于这个主题的内容

  • [Martin Fowler 有一个关于依赖注入/控制反转的精彩文章](https://martinfowler.com.cn/articles/injection.html)
  • [关于依赖倒置原则的维基百科文章](https://en.wikipedia.org/wiki/Dependency_inversion_principle)

入门

组织您的应用程序

首先,编写一个使用 struct & 类型(作为对 [AutoFac](https://autofac.org/) 的致敬,我移植了他们的经典 "入门" 示例)的经典应用程序。下面使用了代码片段来说明这个小指南,完整的示例可在这里找到。

trait IOutput {
    fn write(&self, content: String);
}

struct ConsoleOutput {
    prefix: String,
    other_param: usize,
}

impl IOutput for ConsoleOutput {
    fn write(&self, content: String) {
        println!("{} #{} {}", self.prefix, self.other_param, content);
    }
}

trait IDateWriter {
    fn write_date(&self);
}

struct TodayWriter {
    output: Box<IOutput>,
    today: String,
    year: String,
}

impl IDateWriter for TodayWriter {
    fn write_date(&self) {
       let mut content = "Today is ".to_string();
       content.push_str(self.today.as_str());
       content.push_str(" ");
       content.push_str(self.year.to_string().as_str());
       self.output.write(content);
    }
}

标记结构体为组件

组件是一个表达式或其他代码片段,它公开一个或多个服务并可以接受其他依赖项。

在我们的示例中,我们有 2 个组件

  • TodayWriter 类型为 IDateWriter
  • ConsoleOutput 类型为 IOutput

为了能够将它们识别为组件,he_di 提供了一个 #[derive)] 宏(尽管使用了 he_di_derive 包)。这只需使用以下属性即可完成。

#[derive(Component)] // <--- mark as a Component
#[interface(IOutput)] // <--- specify the type of this Component
struct ConsoleOutput {
    prefix: String,
    other_param: usize,
}

在当前版本中,您还需要使用 #[interface)] 属性指定您的组件类型。

表达依赖项

某些组件可以依赖其他组件,这使得 DI 逻辑也可以将这些组件注入另一个组件。

在我们的示例中,ConsoleOuput 是一个没有依赖项的组件,而 TodayWriter 是一个依赖于 IOutput 组件的组件。

为了表达这种依赖关系,请在您的结构体中使用#[inject]属性来标记属性,并将属性声明为一个特质对象

在我们的例子中

#[macro_use] extern crate he_di_derive;

#[derive(Component)] // <--- mark a struct as a Component that can be registered & resolved
#[interface(IDateWriter)] // <--- specify which interface it implements
struct TodayWriter {
    #[inject] // <--- flag 'output' as a property which can be injected
    output: Box<IOutput>, // <--- trait object using the interface `IOutput`
    today: String,
    year: usize,
}

应用程序启动

在应用程序启动时,您需要创建一个ContainerBuilder,并将您的组件注册到它上面。

在我们的例子中,我们使用以下方式将ConsoleOutputTodayWriter注册到ContainerBuilder中:

// Create your builder.
let mut builder = ContainerBuilder::new();

builder
    .register::<ConsoleOutput>()
    .as_type::<IOutput>();

builder
    .register::<TodayWriter>()
    .as_type::<IDateWriter>();

// Create a Container holding the DI magic
let mut container = builder.build().unwrap();

Container引用是您稍后用于解析类型和组件的内容。您可以根据需要将其存储。

应用程序执行

在应用程序执行过程中,您需要使用您已注册的组件。您可以通过使用3个resolve()方法之一从Container中解析它们。

传递参数

在大多数情况下,您需要向组件传递参数。这可以在将组件注册到ContainerBuilder时或从Container中解析组件时完成。

您可以使用属性名称或属性类型注册参数。在后一种情况下,您需要确保它是唯一的。

在注册组件时

在注册时传递参数使用with_named_parameter()with_typed_parameter()链式方法,如下所示:

builder
    .register::<ConsoleOutput>()
    .as_type::<IOutput>()
    .with_named_parameter("prefix", "PREFIX >".to_string())
    .with_typed_parameter::<usize>(117 as usize);

在解析组件时

在解析时传递参数使用您Container实例相同的with_named_parameter()with_typed_parameter()方法。

对于我们的示例应用程序,我们创建了一个write_date()方法,从容器中解析写入器,并演示了如何使用其名称或类型传递参数。

fn write_date(container: &mut Container) {
    let writer = container
        .with_typed_parameter::<IDateWriter, String>("June 20".to_string())
        .with_named_parameter::<IDateWriter, usize>("year", 2017 as usize)
        .resolve::<IDateWriter>()
        .unwrap();
    writer.write_date();
}

现在,当您运行程序时...

  • write_date()方法请求he_di提供IDateWriter
  • he_di看到IDateWriter映射到TodayWriter,因此开始创建一个TodayWriter
  • he_di看到TodayWriter的构造函数需要一个IOutput
  • he_di看到IOutput映射到ConsoleOutput,因此创建了一个新的ConsoleOutput实例。
  • 由于ConsoleOutput没有更多的依赖关系,它使用此实例来完成TodayWriter的构造。
  • he_di将完全构造的TodayWriter返回给write_date()以供使用。

稍后,如果我们想使我们的应用程序写入不同的日期,我们只需实现一个不同的IDateWriter,然后更改应用程序启动时的注册。我们不需要更改任何其他类。太棒了,这是控制反转!

路线图

此crate的当前实现仍然是WIP。一些已确定的限制性知识(将被进一步探索)是:

  • #[derive(Component)] 应该针对复杂情况进行测试,并需要编写更多测试(例如,带有生命周期的结构体、泛型等)
  • 我们应该支持闭包作为创建参数的方式(在注册或解析时)

依赖关系

~0.2–1MB
~27K SLoC