使用容器实现控制反转 IOC(也称为依赖注入)






3.45/5 (6投票s)
控制反转(IOC,又名通过依赖注入(DI))的概念与实现
使用 IOC(依赖注入)和策略模式,在您的子系统(微服务)中松散耦合您的实现对象。
引言
本文提供了控制反转(IOC,又名通过依赖注入(DI))的概念与实现。
它结合了策略模式与接口和实现类。之后,接着是 Java(使用 Spring Boot)和 .NET C#(使用 Autofac)中的示例。
简而言之,基于策略模式的 IOC (DI) 允许 OO 类在组件实现内部实现非常松散的耦合。这使得代码重构变得更容易,类的重用性更好,并且(如果设计得当)更好地分离关注点,以及更容易调整组件以适应新的或更改的功能(灵活性和适应更改的便捷性)。因此,尽管它不是将组件松散耦合在一起的模式,但它确实有助于在其他组件或应用程序中重用使用这些模式创建的(例如,二进制)库。
参阅相关文章
本文是如何构建的?
- Java 和 C# 中策略和 IOC(通过依赖注入)的最终用法示例
- 策略模式概念
- IOC 与 DI 的概念(IOC,字段和构造函数注入)
- Java 实现细节 Spring Boot
- C# 实现细节与 AutoFac
- 总结与结论,以及更多
阅读本文之前
我将展示 Java 和 C# 中的策略模式。Java 中基于策略的 IOC(依赖注入)通过 Spring Boot ApplicationContext 容器和 @Autowiring 进行演示。
C# 中基于策略的 IOC(依赖注入)通过 Autofac 进行演示。
注意:我们中间的纯粹主义者当然会说,“DI 不等同于 IOC”,它是 IOC 的“一种实现”。谨记。另见 [2]。
Java 中的策略模式最终用法示例 (1+)
//
//An example of the usage
//
Contract contract = new ContractImpl("cloe");
//
//or
//
Contract contract = new ContractImpl();
contract.getPerson().setName("roland");
C# 中的策略模式最终用法示例 (4+)
//
//An example of the usage in C#
IContract contract = new Contract("cloe");
//
//or
//
IContract contract = new Contract();
contract.Person().Name = "roland";
Java 中的 IOC 模式最终用法示例 (1+)
//
//An example of the usage in Java
//
@Autowired
public Contract contract;
public void run(String... args) {
contract.getPerson().setName("roland");
}
C# 中的 IOC 模式最终用法示例 (4+)
//
//An example of the usage in C#
//
IContract contract = container.Resolve<IContract>();
contract.Person().Name = "roland";
策略模式概念
这种模式的思想是一个基本的 OO 思想:将类实现与其接口分离。因此,所有“公共”行为都通过类的接口公开。
在本文中,我选择了“干净”的实现,并将接口与类分离到单独的库中。
我总是建议这种实现。这不是本文详细探讨这方面的内容。但请相信我,这会增加一点开销,但在大型系统中,如果您不这样做,最终会陷入复杂的构建依赖关系。
接口,接口,接口……
基本上,有以下步骤
- 在接口中定义您的
public
方法 - 在类中实现接口
- 在接口级别而不是实现级别定义您的关联和/或聚合
策略模式的优点
策略模式有大量重要的优点,特别是在大型系统中
- 构建依赖关系更容易解决:首先构建包含所有接口的组件,然后构建实现组件
- 单元测试中的可测试性更容易。您可以更容易地模拟接口,而不是实现
- 接口和实现的分离允许有多种实现
- 依赖注入更容易实现
- 如果您这样做,面向切面编程和使用代理的拦截器(参见 [3])是可能的。如果您不这样做,则不可能。
策略模式的缺点
- 您需要定义两次
public
方法和属性(一个在类中,一个在接口中),这使得重构工作量稍大 - 您可能会发现代码更难阅读。毕竟,接口背后会是什么实现?
注意:在实现策略模式的代码中,Java 和 C# 几乎没有区别。概念上,100% 相同。小差异是
- C# 的编码标准是默认接口 I... (
IContract
,Contract
) - Java 的编码标准是默认类 ...Impl (
Contract
,ContractImpl
) - C# 有
{get;set;}
语法(String Name {get;set;})
- Java 有
Type get...()
和set..(Type value)
语法 (String getName() void setName(String name)
)。
Java 中的策略模式实现 (1+)
//
// Implementation and Interface separation: Strategy pattern example
//
//
// The Person interface
//
public interface Person {
String getName();
void setName(String name);
}
//
// The Person implementation
//
public class PersonImpl implements Person {
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
//
// The Contract interface
//
public interface Contract {
Person getPerson();
void setPerson(Person person);
}
//
// The Contract implementation
//
public class ContractImpl implements Contract{
private Person person = new PersonImpl();
public ContractImpl(){
}
public ContractImpl(String personName){
this.person.setName(personName);
}
public Person getPerson(){
return person;
}
public void setPerson(Person person){
this.person = person;
}
}
...
//An example of the usage
Contract contract = new ContractImpl("cloe");
//or
Contract contract = new ContractImpl();
contract.getPerson().setName("roland");
C# 中的策略模式实现示例 (1+)
//
// The Person interface
//
public interface IPerson
{
String Name {get; set;}
}
//
// The Person implementation
//
public class Person : IPerson
{
public String Name {get; set;}
}
//
// The Contract interface
//
public interface IContract
{
IPerson Person {get; set;}
}
//
// The Contract implementation
//
public class Contract : IContract
{
public Contract(){}
public Contract(String personName)
{
this.person.Name = personName;
}
public IPerson Person {get; set;} = new Person();
}
...
...
//An example of the usage
IContract contract = new Contract("cloe");
//or
IContract contract = new Contract();
contract.Person.Name = "roland";
Java 中的 IOC 模式示例 (1.7+, Spring Boot)
现在,只有实现改变,接口不变
//
// The Person implementation: @Component lets the PersonImpl being found for a @ComponentScan,
// to register the interface to the Container Spring boot context
//
@Component
public class PersonImpl implements Person {
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
//
// The Contract implementation
//
@Component
public class ContractImpl extends Contract{
@Autowired
private Person person;
public Person getPerson(){
return person;
}
}
...
//An example of the usage
@ComponentScan()
@SpringBootApplication()
public class IOCApplication {..}
...
public void run(String... args) {
//
//note the type Contract being a interface Contract.class type!
//
Contract contract = context.getBean(Contract.class);
contract.getPerson().setName("roland");
}
//or, and note the type Contract being a interface type!
...
@Autowired
public Contract contract;
public void run(String... args) {
contract.getPerson().setName("roland");
}
C# 中的 IOC 模式示例 (C# 7+, Autofac)
现在,C# 中的代码需要在一点上进行更改,并且我们需要进行一些对象注册。
注意 1:这是自动字段注入的一个示例,Autofac 通过 ContainerBuilder.PropertiesAutowired();
支持。
注意 2:在下载代码中,有两个实现,一个用于策略,一个用于 IOC DI,并且每个都有一个测试项目。
public IPerson Person { get; set; } //= new Person(); => Injected by Container
ContainerBuilder containerBuilder = new ContainerBuilder();
//
// Register Person and Contract on its interfaces
//
containerBuilder
.RegisterType<Contract>()
.As<IContract>()
.PropertiesAutowired();
containerBuilder
.RegisterType<Person>()
.As<IPerson>()
.PropertiesAutowired();
IContainer container = containerBuilder.Build();
using (var scope = container.BeginLifetimeScope())
{
//
//An example of IContract the usage
//
IContract contract = scope.Resolve<IContract>();
//
// Check that container injected the Person
//
Assert.IsNotNull(contract.Person);
contract.Person.Name = "cloe";
Assert.AreEqual("cloe", contract.Person.Name);
}
现在,虽然我个人对自动字段注入没问题,因为我是一个务实的人,但有些纯粹主义者更喜欢构造函数注入。而且,确实存在我们需要它或者它能让世界更清晰的情况。我建议总是使用构造函数注入,但选择你的方式并学会适应它。
优点是
- 构造函数 DI 代码直接揭示所有依赖关系
- 构造函数 DI 可用于进行额外的初始化
我没意见,给你
...
//
// CTOR Dependency injection.
// So, the container "sees" a IPerson is needed during factoring, and factors that.
//
public Contract(IPerson person)
{
this.Person = person;
}
public Contract(IPerson person, String name)
{
this.Person = person;
person.Name = name;
}
...
//
// Note the missing .PropertiesAutowired(); in the registration.
// Injection of IPerson now on CTOR.
//
ContainerBuilder containerBuilder = new ContainerBuilder();
containerBuilder
.RegisterType<Contract>()
.As<IContract>();
containerBuilder
.RegisterType<Person>()
.As<IPerson>();
...rest code same...
//but this uses the person/name ctor. Person is factored and injected by the container.
List<Parameter> parameters = new List<Parameter>();
parameters.Add(new NamedParameter("name", "cloe"));
contract = scope.Resolve<IContract>(parameters);
到此为止,C# 中使用 Autofac 的策略和 IOC DI,包括字段注入和构造函数注入。
注意 1:Autofac 有注册模块概念。请使用它们。请参阅我的其他文章 [3] 和 [4] 中附带的代码。
结论
我们从本文中学到了什么?
- 策略模式在 C# 和 Java 中是可行的,几乎 100% 相同。
- IOC DI 在 C# 和 Java 中都是可行的,同时支持构造函数注入和字段注入。
- 我们看到现在添加具有相同接口的两个实现非常简单(请参阅附带的代码)。
关注点
现代应用程序,无论是应用程序、微服务还是库,几乎都使用容器和 IOC。我没有详细介绍容器的各种特性和概念,比如作用域和生命周期,以及无数其他功能。互联网上有很多示例。除了 Autofac(Pico、Castle Windsor 等)之外,我也没有详细介绍策略模式的测试优点,但我希望能写一篇关于分层单元测试的文章,展示这一点。
关于如何通过一些技巧正确组织代码的建议
如果您发现您的构造函数中有超过 2 或 3 个依赖项,您可能需要重构您的代码。很有可能您的类承担了不止一个职责。那么请拆分您的类,让调用者使用两个协同工作的类。另一个解决方案是使用外观模式,您将代码重构到一个额外的类,该类具有一部分依赖项,并将其包含在您的构造函数中。然后您构建一些分层的“管道”。这很好。无论哪种方式:每个类的依赖项最多保持在 [2-4] 个左右。
我确实想指出,我猜我大约在七年前开始使用 IOC DI 容器编写代码。它完全改变了我的编码世界。并且,需要一段时间(大约两个月)才能适应它。但是,自从我切换以来,我敢说,我现在可以编写真正的 OO 代码了。并不是我一直都这样做 :-),但我现在可以了……我以前做的事情实际上根本就不是那样的。
接下来,关于使用 IOC 和 DI 编写大型系统或组件的注意事项。我发现 IOC DI 代码按类阅读起来相当容易,但在整个组件中跟踪代码却非常困难。几乎不可能。而这正是好消息!因为解决方案很简单:使用 UML 类图和序列图以及其他图来记录您的组件!UML 提供了揭示代码结构和流程的所有可能性。请使用它的强大功能吧?
而且,如果您讨厌 UML 绘图工具(我讨厌),另一个建议:开始使用 PlantUML!另请参阅本文附带的 UML 代码。不要让它成为您不编写代码 UML 的借口。那简直是不专业、懒惰或两者兼而有之。
历史
- 2020 年 4 月 19 日:初始版本