65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.45/5 (6投票s)

2020年4月20日

CPOL

7分钟阅读

viewsIcon

7057

downloadIcon

72

控制反转(IOC,又名通过依赖注入(DI))的概念与实现

Sample Image - maximum width is 600 pixels

使用 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+)

Sample Image - maximum width is 600 pixels

//
//An example of the usage in C#
IContract contract = new Contract("cloe");
//
//or
//
IContract contract = new Contract();
contract.Person().Name = "roland";

Java 中的 IOC 模式最终用法示例 (1+)

Sample Image - maximum width is 600 pixels

//
//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 日:初始版本
© . All rights reserved.