目录
面试求生教程
本文的目标是提供有关依赖项注入模式和相关主题的简短教程。对于那些想要了解该主题的人来说,它可以用作“第一次联系教程”,也可以用作那些想要更新知识的人的“复习材料”。它可以作为那些需要快速掌握基本概念的人的面试准备材料。
本教程中涵盖的主题通常会要求面试高级(.NET)开发人员职位的候选人。
这是一个基本教程,不涉及所有细节。人们在“同心圆”中呈现知识时学习得最好,在第一个圆中,他们被教导一切的基础,在第二个同心圆中,他们回顾他们在前一个圆圈中学到的知识,并用更多细节扩展该知识,然后在下一个圆圈中他们再次做类似的事情,等等。本文旨在为感兴趣的读者提供该主题的第一遍。
提出的主题
介绍以下主题:
- 依赖关系注入模式——DI(*)
- 依赖反转原理——DIP(**)
- 控制反转——IoC(****)
- 依赖关系注入容器(****)
依赖关系注入模式——DI(*)
首先,“依赖注入模式”是一种软件设计模式。它被称为“模式”,因为它建议对特定问题进行低级的特定实现。
此模式旨在解决的主要问题是如何创建“松散耦合”组件。它通过将组件的创建与其依赖项分开来实现这一点。
此模式中有四个主要角色(类):
- Client:客户端是一个组件/类,它想要使用另一个组件提供的服务,称为Service。
- Service-Interface:服务接口是描述服务组件提供的服务类型的抽象。
- Service:Service组件/类正在根据服务接口描述提供服务。
- Injector:是一个组件/类,其任务是创建客户端和服务组件并将它们组合在一起。
它的工作方式是客户端依赖于服务接口IService。客户端依赖于IService接口,但不依赖于服务本身。服务IService实现接口并提供客户端需要的某些服务。注入器创建Client和Service并将它们组装在一起。我们说注入器将服务“注入”到客户端。
下面是此模式的类图:
下面是此模式的示例代码:
public interface IService
{
void UsefulMethod();
}
public class Service : IService
{
void IService.UsefulMethod()
{
//some useful work
Console.WriteLine("Service-UsefulMethod");
}
}
public class Client
{
public Client(IService injectedService = null)
{
//Constructor Injection
_iService1 = injectedService;
}
private IService _iService1 = null;
public void UseService()
{
_iService1?.UsefulMethod();
}
}
public class Injector
{
public Client ResolveClient()
{
Service service = new Service();
Client client = new Client(service);
return client;
}
}
internal class Program
{
static void Main(string[] args)
{
Injector injector = new Injector();
Client cli = injector.ResolveClient();
cli.UseService();
Console.ReadLine();
}
}
基于注入方法的依赖注入类型
通常在文献[1]中,人们可以发现他们提到了不同类型的依赖注入,根据将服务注入客户端的方法进行分类。我认为这不是一个重要的区别,因为效果总是相同的,即无论如何,对服务的引用都会传递给客户。但是,为了完整起见,让我们解释一下。
因此,依赖注入的类型是:
- 构造函数注入——注入是在Client构造函数中完成的
- 方法注射——通过专用方法完成注射
- 属性注入——通过public属性进行注射
下面是演示每种类型的代码:
public interface IService
{
void UsefulMethod();
}
public class Service : IService
{
void IService.UsefulMethod()
{
//some useful work
Console.WriteLine("Service-UsefulMethod");
}
}
public class Client
{
public Client(IService injectedService = null)
{
//1.Constructor Injection
_iService1 = injectedService;
}
public void InjectService(IService injectedService)
{
//2.Method Injection
_iService1 = injectedService;
}
public IService Service
{
//3.Property Injection
set { _iService1 = value; }
}
private IService _iService1 = null;
public void UseService()
{
_iService1?.UsefulMethod();
}
}
public class Injector
{
public Client ResolveClient()
{
Service S = new Service();
//NOTE: This is tutorial/demo code, normally you
//implement only one of these three methods
//1.Constructor Injection
Client C = new Client(S);
//2.Method Injection
C.InjectService(S);
//3.Property Injection
C.Service = S;
return C;
}
}
要点——客户端不知道注入的服务类型
让我们强调这种设计模式中的主要内容。这就是Client完全不知道注入Service类型的事实,它只看到IService接口并且不知道注入的Service版本是什么。让我们看一下下面的类图:
Client不知道正在注入哪个服务,如果是Service1,Service2或者Service3。这就是想要的,我们看到组件/类Client, Service1, Service2,和Service3是“松散耦合的”。
Client类现在更具可重用性和可测试性。此功能的一个典型用法是,在生产环境中,Client注入了真实的Service1服务,而在测试中,环境Client注入了Service2,这是一个专为测试而创建的模拟服务。
此模式的优点
此模式的好处是:
- 创建松散耦合的组件/类Client和Service
- 客户端对服务没有依赖性,也不了解,这使得它更可重用和可测试。
- 支持不同开发人员/团队并行开发组件/类客户端和服务,因为它们之间的边界由IService接口明确定义
- 它简化了组件的单元测试
这种模式带来的缺点是:
- 在规划、创建和维护接口方面投入更多精力
- 依赖注入器来组装组件/类
类似模式
这种模式与GoF书籍战略模式[2]非常相似。类图实际上是相同的。区别在于意图:
- 依赖注入更像是结构模式,其目的是组装松散耦合的组件,一旦组装,它们通常在客户端生命周期内保持这种状态;而
- 策略模式是行为模式,其目的是为问题提供不同的算法,这些算法通常在客户端生命周期内可以互换。
依赖反转原则——DIP(**)
因此,“依赖反转原则(DIP)”是一个软件设计原则。它被称为“原则”,因为它提供了有关如何设计软件产品的高级建议。
DIP是Robert C. Martin [5]提倡的首字母缩略词SOLID [3]下的五个设计原则之一。DIP原则规定:
- 高级模块不应依赖于低级模块。两者都应该依赖于抽象。
- 抽象不应依赖于细节。细节应该取决于抽象。
解释是:
虽然高级原则讨论的是“抽象”,但我们需要将其转换为特定编程环境(在本例中为C#/.NET)中的术语。C#中的抽象是通过接口和抽象类实现的。当谈到“细节”时,原则意味着“具体实现”。
因此,基本上,这意味着DIP促进了C#中接口的使用,而具体的实现(低级模块)应该依赖于接口。
传统的模块依赖项如下所示:
DIP提出了这种新设计:
如您所见,某些依赖项(箭头)的方向倒置,因此这就是名称“反转”的由来。
DIP的目标是创建“松散耦合”的软件模块。传统上,高级模块依赖于低级模块。DIP的目标是使高级模块独立于低级模块的实现细节。它通过在它们之间引入“抽象层”(以接口的形式)来实现这一点。
依赖注入模式(*)遵循此原则,并且经常被提及与DIP实现密切相关。但DIP原则是一个更广泛的概念,对其他设计模式有影响。例如,当应用于工厂设计模式或单一实例设计模式时,它建议这些模式应返回对接口的引用,而不是对对象的引用。
控制反转——IoC(****)
同样,“控制反转(IoC)”是一个软件设计原则。它被称为“原则”,因为它提供了有关如何设计软件产品的高级建议。
在传统编程中,自定义代码始终具有流控制并调用库来执行任务。
IoC原则建议(有时)将控制流提供给库(“框架”),这些库将调用自定义代码来执行任务。
当他们说“框架”时,他们指的是为特定任务设计的专用的、任意复杂的可重用模块/库,并且自定义代码的编写方式使其可以与该“框架”一起使用。我们说“控制流是颠倒的”,因为现在“框架”调用自定义代码。
框架在控制应用程序活动方面扮演主程序的角色。程序的主要控制是颠倒的,从你移到框架。控制反转是使框架不同于库的关键部分([26])。
IoC原则促进开发和使用实现常见方案的可重用“软件框架”。然后,编写特定于问题的自定义代码,并与“框架”一起解决特定任务。
虽然IoC原则经常在它后面的依赖注入模式(*)的上下文中提到,但它是一个更广泛的概念。例如,基于事件处理程序/回调方法的“UI框架”也遵循IoC原则。有关更多解释,请参见[26],[25],[8]。
依赖注入模式(*)遵循此原则,因为通常的传统方法是让客户端创建服务并建立依赖关系。这里的控制是颠倒的,即服务的创建和依赖关系的创建被委托给注入器,在这种情况下是“框架”。
依赖关系注入容器(****)
因此,“依赖注入容器(DI容器)”是一个软件模块/库,它支持具有许多高级选项的自动依赖注入。
在IoC原则(****)的术语中,DI容器具有“框架”的角色,因此您经常会看到它被称为“DI框架”,但我的观点是“框架”一词被过度使用并导致混淆(你有ASP MVC框架、DI框架、实体框架等)。
在文献中,它通常被称为“IoC容器”,但我认为 IoC 原则(***)是一个比DI模式(*)更广泛的概念,在这里我们实际上是大规模地采用DI模式实现。因此,“DI容器”是一个更好的名称,但名称“IoC容器”非常流行,并且广泛用于同一件事。
什么是DI容器
还记得DI模式(*)和注入器的作用吗?因此,DI容器是一个高级模块/库,可同时充当许多服务的注入器。它支持大规模实现DI模式,具有许多高级功能。DI容器是一种非常流行的体系结构机制,许多流行的框架(如ASP MVC)计划并启用DI容器的集成。
最受欢迎的DI容器是Autofac[10],Unity [15],Ninject [16],Castle Windsor [17]等。
DI容器函数
一个DI容器将提供的典型功能包括:
Register 映射。您需要告诉抽象(接口)到具体实现(类)之间的DI容器映射,以便它可以正确注入正确的类型。在这里,您将向容器提供工作所需的基本信息。
管理对象作用域和生存期。您需要告诉容器它创建的对象将具有什么范围和生存期。
典型的“生命周期”模式是:
- 单例:始终使用对象的单个实例。
- 瞬态:每次创建对象的新实例时。
- 范围:这通常是每个隐式或显式定义的范围的单一实例模式。
例如,您需要告诉容器是否希望在每次解析依赖项时创建一个新对象,或者是否希望应用单例模式。例如,单例可以是每个进程、每个线程或每个“用户定义的范围”。此外,还需要指定对象的所需生存期。例如,您可以将每个进程的对象生存期或对象生存期配置为每个“用户定义的范围”,这意味着对象将在用户定义的范围结束时释放。容器可以强制执行所有这些,只需要精确配置即可。
解析方法。以下是创建和组装所需对象/类型的实际工作。容器创建特定类型的对象,解析所有依赖项,并将其注入到创建的对象中。该方法以递归方式深入工作,直到解析所有依赖项。DI容器通过使用反射等技术来解析依赖项。
DI容器:自动——简单示例
C#世界中最常用的DI容器之一是Autofac [10]。我们将展示一个简单的例子来说明它是如何工作的。
这是我们的类图:
这是我们的示例代码:
public interface IService
{
}
public class Service : IService
{
public Service()
{
}
}
public class Client
{
public IService iService;
public Client(IService injectedService)
{
iService = injectedService;
}
}
internal class Program
{
public static string MySerialize(object obj)
{
string serialized = null;
var indented = Newtonsoft.Json.Formatting.Indented;
var settings = new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.All
};
serialized = JsonConvert.SerializeObject(obj, indented, settings); //(5)
return serialized;
}
static void Main(string[] args)
{
// Register mappings
var builder = new Autofac.ContainerBuilder();
builder.RegisterType<Service>().As<IService>(); //(1)
builder.RegisterType<Client>().AsSelf(); //(2)
Autofac.IContainer Container = builder.Build(); //(3)
//Resolve object
var client = Container.Resolve<Client>(); //(4)
// Json serialize object to see what we got
Console.WriteLine(MySerialize(client)); //(6)
Console.ReadLine();
}
}
这是执行结果:
如您所见,Autofac有自己的API,我们需要遵循。在(1)处,我们注册了映射IService->Service。然后在(2)中,我们注册了客户端本身。在(3)中,我们构建容器并准备使用。在(4)中,我们进行解析,这就是完成依赖和注入解析的地方。
为了验证我们是否得到了想要的对象,我们在(5)处序列化它并在(6)处打印出来。
如果你再看一下那里的(*)和术语,那么我们的Client类具有(*)中的“Client”角色,Service类具有(*)中的“Service”角色,而Container对象具有(*)中的“Injector”角色。
只是一个简短的说明。这是一个教程——概念代码演示。我们在上面的示例中使用DI容器的方式,通过在顶层显式请求依赖项解析,使其有点类似于服务定位器模式[29]。DI容器的正确用法是将其用作框架,而不是显式请求解析依赖项。
DI容器:自动——深度依赖示例
现在我们将展示一个更复杂的示例,其中包含深度依赖树。下面是新的类图。
下面是新的类图:
这是我们的示例代码:
public class C
{
public IS obj_is = null;
public IT obj_it = null;
public C(IS injectIs, IT injectIT)
{
obj_is = injectIs;
obj_it = injectIT;
}
}
public interface IS
{
}
public class S : IS
{
public IU obj_iu = null;
public IV obj_iv = null;
public S(IU injectIU, IV injectIV)
{
obj_iu = injectIU;
obj_iv = injectIV;
}
}
public interface IT
{
}
public class T : IT
{
public IZ obj_iz = null;
public T(IZ injectIZ)
{
obj_iz = injectIZ;
}
}
public interface IU
{
}
public class U : IU
{
}
public interface IV
{
}
public class V : IV
{
public IX obj_ix = null;
public V(IX injectIX)
{
obj_ix = injectIX;
}
}
public interface IZ
{
}
public class Z : IZ
{
}
public interface IX
{
}
public class X : IX
{
}
internal class Program
{
public static string MySerialize(object obj)
{
string serialized = null;
var indented = Newtonsoft.Json.Formatting.Indented;
var settings = new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.All
};
serialized = JsonConvert.SerializeObject(obj, indented, settings);
return serialized;
}
static void Main(string[] args)
{
// Register mappings
var builder = new Autofac.ContainerBuilder();
builder.RegisterType<S>().As<IS>();
builder.RegisterType<T>().As<IT>();
builder.RegisterType<U>().As<IU>();
builder.RegisterType<V>().As<IV>();
builder.RegisterType<Z>().As<IZ>();
builder.RegisterType<X>().As<IX>();
builder.RegisterType<C>().AsSelf();
Autofac.IContainer Container = builder.Build();
//Resolve object
var c = Container.Resolve<C>();
// Json serialize object to see what we got
Console.WriteLine(MySerialize(c));
Console.ReadLine();
}
}
这是执行结果:
从执行结果可以看出,Autofac DI容器再次完成了它的工作。请注意,例如,S类在DI模式(*)的术语中同时是“Client”和“Service”。它是C类的“Service”,类U和V的“Client”。对象容器在(*)的术语中扮演“Injector”的角色
再次,一个简短的说明。这是一个教程 – 概念代码演示。我们在上面的示例中使用 DI 容器的方式,通过在顶层显式请求依赖项解析,使其有点类似于服务定位器模式 [29]。DI 容器的正确用法是将其用作框架,而不是显式请求解析依赖项。
DI容器:Autofac——配置对象作用域和生存期
如果我们看上面的例子,会出现一个问题。如果我们有两个C类的对象,对象c1和c2,由DI容器通过解析生成,这些对象是不同还是相同?那么依赖对象呢,比如T类的对象,我们称它们为t1和t2,它们都是不同的还是相同的?
//let us assume we created two objects, c1 and c1
var c1 = Container.Resolve<C>();
var c2 = Container.Resolve<C>();
//are they same or different?
//what will this give us as result?
bool sameObjects=(c1 == c2);
答案是:这是可配置的。但是由于我们没有在前面的示例中对其进行配置,因此我们将获得默认行为,即每次创建一个新对象。在这个例子中,对象c1和c1是不同的,因为所有类S、T、U、V、Z和X的依赖对象都是不同的。
为DI容器Autofac([11]、[12])配置对象范围和生存期的典型选项包括:
- 每个依赖项的实例数:在文献中也经常称为“瞬态”。这是一种“每次新对象”模式。基本上,这意味着每次请求对象时,都会创建一个新实例。这是默认行为。
- 单个实例:也称为“单一实例”。这基本上是“每个进程的单例”。每次在请求解析的过程中,您都会获得对象的相同实例。
- 每个生存期范围的实例数:即“每个用户定义的单例范围”。用户需要指定范围,在其中,他将始终获得相同的实例。
- 每个匹配生存期范围的实例数:这又是单例,但这次是“每个用户定义的命名作用域的单例”。用户需要使用定义的命名范围,并且每次在其中时都会获得对象的相同实例。
- 每个请求的实例数:在ASP类型的应用程序中,它会导致“每个请求的单一实例”。这实际上与4相同,只是每个请求创建命名范围。更多解释见[12]。
- 每个拥有的实例:这有点复杂,所以我们不会在这里详细介绍。更多解释见[12]。
- 线程作用域:它不作为单独的配置选项存在,而是依赖于3在线程方法中创建命名作用域,并将其实现为“每个生存期作用域的实例数”解决方案。
下面是上面列出的配置选项在代码中的外观的一些示例:
//1. Instance Per Dependency
builder.RegisterType<Worker>(); // using defualt behaviour
//explicit definition
builder.RegisterType<Worker>().InstancePerDependency();
//2. Single Instance
builder.RegisterType<Worker>().SingleInstance();
//3. Instance Per Lifetime Scope
builder.RegisterType<Worker>().InstancePerLifetimeScope();
using (var scope1 = container.BeginLifetimeScope())
{
var w1 = scope1.Resolve<Worker>();
var w2 = scope1.Resolve<Worker>();
// w1 and w2 are the same instance
}
//4. Instance Per Matching Lifetime Scope
builder.RegisterType<Worker>()
.InstancePerMatchingLifetimeScope("MyScope");
using (var scope1 = container.BeginLifetimeScope("MyScope"))
{
var w1 = scope1.Resolve<Worker>();
}
using (var scope2 = container.BeginLifetimeScope("MyScope"))
{
var w2 = scope2.Resolve<Worker>();
// w1 and w2 are the same instance
}
//5. Instance Per Request
builder.RegisterType<Worker>().InstancePerRequest();
//6. Instance Per Owned
builder.RegisterType<OwnerClass>();
builder.RegisterType<SlaveClass>().InstancePerOwned<OwnerClass>();
//7. Thread Scope
//similar to 3.
我们不会更详细地介绍或提供代码示例,因为这对本文来说太多了。
DI容器——与应用程序框架集成
DI容器是一种非常流行的体系结构机制,许多应用程序框架计划并启用与DI容器的集成。
例如,ASP.NET MVC框架公开了潜在客户DI容器需要实现的接口IDependencyResolver [13]。与Autofac集成的示例如下所示:
// ASP.NET MVC and Autofac integration
// Context:
// - build Autofac DI Container
// var builder = new Autofac.ContainerBuilder();
// Autofac.IContainer container = builder.Build();
// - container implements interfaces Autofac.IContainer
// and Autofac.IComponentContext
// - new AutofacDependencyResolver(container) implements
// System.Web.Mvc.IDependencyResolver
// - System.Web.Mvc provides a registration point for
// dependency resolvers with method
// public static void System.Web.Mvc.DependencyResolver
// .SetResolver(System.Web.Mvc.IDependencyResolver resolver)
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
因此,重点是MVC框架ASP.NET为依赖项解析器提供了注册点。如果您不想使用DI,那也没关系。但是,如果要在ASP.NET MVC应用程序中使用DI,则可以向应用程序框架注册所选的DI容器,如上所示,DI解析将神奇地开始在您的应用程序中工作。
有关如何将Autofac DI容器与其他应用程序集成的信息,请参阅[27]。这对于本教程文章来说已经足够了。
结论
在本文中,我们重点介绍了依赖注入模式(DI)及其工业应用依赖注入容器(又名IoC容器)。我们还解释了软件设计的相关原则,依赖反转原则(DIP)和控制反转(IoC)原则。我们展示了一些使用Autofac容器的示例代码。重点是读者理解和欣赏DI容器在现代应用程序中的实际用法的能力。
DI模式和DI容器是当今软件架构的主流技术,并且将继续存在。
在本教程中,我们对适合需要快速掌握概念的读者的材料进行了简明扼要的概述。建议进一步阅读有关这些主题的内容。一些初学者的教程是[14],[18]——[23]。严肃文学是[9],[8],[26]。
引用
- [1] https://en.wikipedia.org/wiki/Dependency_injection
- [2] https://en.wikipedia.org/wiki/Strategy_pattern
- [3] https://en.wikipedia.org/wiki/SOLID
- [4] https://en.wikipedia.org/wiki/Dependency_inversion_principle
- [5] https://en.wikipedia.org/wiki/Robert_C._Martin
- [6] https://en.wikipedia.org/wiki/Inversion_of_control
- [7] https://en.wikipedia.org/wiki/Service_locator_pattern
- [8] Inversion of Control Containers and the Dependency Injectionpattern
- [9] Mark Seemann, Steven van Deursen - Dependency Injection Principles, Practices, and Patterns, Manning Publications, 2019
- [10] Autofac: Home
- [11] Controlling Scope and Lifetime — Autofac 6.0.0 documentation
- [12] Instance Scope — Autofac 6.0.0 documentation
- [13] IDependencyResolver Interface (System.Web.Mvc) | Microsoft Learn
- [14] An Absolute Beginner's Tutorial on Dependency Inversion Principle, Inversion of Control and Dependency Injection - CodeProject
- [15] Unity Container Introduction | Unity Container
- [16] Ninject - Open source dependency injector for .NET
- [17] Windsor | Castle Project
- [18] Dependency Inversion Principle, IoC Container, and Dependency Injection: Part 1 - CodeProject
- [19] Dependency Inversion Principle, IoC Container & Dependency Injection: Part 2 - CodeProject
- [20] Dependency Inversion Principle, IoC Container, and Dependency Injection: Part 3 - CodeProject
- [21] Dependency Inversion Principle, IoC Container, and Dependency Injection: Part 4 - CodeProject
- [22] Dependency Inversion Principle, IoC Container, and Dependency Injection: Part 5 - CodeProject
- [23] A Curry of Dependency Inversion Principle (DIP), Inversion of Control (IoC), Dependency Injection (DI) and IoC Container - CodeProject
- [24] What is Inversion of Control?
- [25] What is inversion of control (IoC)?
- [26] InversionOfControl
- [27] Application Integration — Autofac 6.0.0 documentation
- [28] .Net Core Dependency Injection
- [29] https://en.wikipedia.org/wiki/Service_locator_pattern
https://www.codeproject.com/Articles/5333947/Dependency-Injection-Pattern-in-Csharp-Short-Tutor