想必做过中型以上工程项目的小伙伴都听说过依赖倒置、控制反转、依赖注入等软件工程概念。能够熟悉使用抽象与依赖倒置在工程开发上会有很多好处,比如提高代码复用性、实现真正的单元测试、减少修改模块的必要等。这次为大家介绍一个Rust
中辅助依赖注入的库。
Shaku 是一个依赖注入库。亦可单独直接使用也可与其他应用框架整合使用,比如Rocket (请参照 shaku_rocket
).
注意: 本入门指南重点介绍在应用程序(或技术上说,容器)的整个生命周期内都有效的组件。阅读此入门指南后,请查看provider
模块,以了解如何创建寿命较短的服务。
从应用程序的结构和特征开始。使用Arc
作为依赖项。
use std::sync::Arc;
trait IOutput {
fn write(&self, content: String);
}
trait IDateWriter {
fn write_date(&self);
}
struct ConsoleOutput;
impl IOutput for ConsoleOutput {
fn write(&self, content: String) {
println!("{}", content);
}
}
struct TodayWriter {
output: Arc,
today: String,
year: usize,
}
impl IDateWriter for TodayWriter {
fn write_date(&self) {
self.output.write(format!("Today is {}, {}", self.today, self.year));
}
}
接口特征需要一定的界限,例如,使用thread_safe
功能时,必须使用 static
和 Send + Sync
。Interface
特征可作为这些界限的特征别名,并会自动实现那些实现了界限的类型。
在我们之前示例中,两个接口特征将变为:
use shaku::Interface;
trait IOutput: Interface {
fn write(&self, content: String);
}
trait IDateWriter: Interface {
fn write_date(&self);
}
组件是实现接口特征的结构。在我们的示例中,我们有2个组件:
TodayWriter
类型为 IDateWriter
ConsoleOutput
类型为 IOutput
这些组件必须实现 Component
, 既可以手动实现或者使用派生宏实现:
use shaku::Component;
#[derive(Component)]
#[shaku(interface = IOutput)]
struct ConsoleOutput;
组件可以依赖于其他组件,在我们的示例中, TodayWriter
依赖于 IOutput
组件。
要想表达这个依赖关系,首先确保该属性被声明为包装在Arc
中的特征对象
。然后(如果使用派生宏的方式)在该属性上使用#[shaku(inject)]
声明告知shaku来注入依赖项。
我们的示例将做如下修改:
use shaku::Component;
#[derive(Component)]
#[shaku(interface = IDateWriter)]
struct TodayWriter {
#[shaku(inject)]
output: Arc,
today: String,
year: usize,
}
如果你没有使用派生宏, 在Component::dependencies
里返回Dependency
对象并且将它们通过Component::build
进行手动注入。
在应用程序启动入口处,创建ContainerBuilder
并用它注册所有组件。它会生成一个可以用来解析组件的Container
。
use shaku::ContainerBuilder;
let mut builder = ContainerBuilder::new();
builder.register_type::();
let container = builder.build().unwrap();
大多数情况下你需要吧参数传进一个组件。这可以通过向ContainerBuilder
注册一个组件来完成。
你可以使用属性名称或属性类型来注册参数。如果用属性类型的话,需要确保类型的独特性。
通过with_named_parameter
和with_typed_parameter
来传参:
builder
.register_type::()
.with_named_parameter("today", "Jan 26".to_string())
.with_typed_parameter::(2020);
在程序运行时,你会需要使用你所注册过的组件。这可以通过从Container
中使用其中的某一个resolve
方法来完成。
比如,我们可以这样将示例中的日期打印出来:
let writer: &dyn IDateWriter = container.resolve_ref().unwrap();
writer.write_date();
总结一下,在你运行示例中的程序时:
组件以及它们实例化时所需的参数将会被注册进ContainerBuilder
中。
resolve_ref()
方法会向Container
询问一个实现了IDateWriter
接口的实例。
Container
将会查找对应IDateWriter
接口的实现是TodayWriter
,并返回该组件的实例。
之后如果需要扩充我们的程序,比如我们希望程序能够以不同的方式输出结果,我们只需要再用不同的方式实现接口IOutput
并且在程序运行起始注册组件的地方做出相应的调整使用新完成的实现。这样就可以避免修改其他地方的代码。这就是所谓的控制反转!
use shaku::{Component, ContainerBuilder, Interface};
use std::sync::Arc;
trait IOutput: Interface {
fn write(&self, content: String);
}
trait IDateWriter: Interface {
fn write_date(&self);
}
#[derive(Component)]
#[shaku(interface = IOutput)]
struct ConsoleOutput;
impl IOutput for ConsoleOutput {
fn write(&self, content: String) {
println!("{}", content);
}
}
#[derive(Component)]
#[shaku(interface = IDateWriter)]
struct TodayWriter {
#[shaku(inject)]
output: Arc,
today: String,
year: usize,
}
impl IDateWriter for TodayWriter {
fn write_date(&self) {
self.output.write(format!("Today is {}, {}", self.today, self.year));
}
}
let mut builder = ContainerBuilder::new();
builder.register_type::();
builder
.register_type::()
.with_named_parameter("today", "Jan 26".to_string())
.with_typed_parameter::(2020);
let container = builder.build().unwrap();
let writer: &dyn IDateWriter = container.resolve_ref().unwrap();
writer.write_date();