目录

面试求生教程

提出的主题

依赖关系注入模式——DI(*)

基于注入方法的依赖注入类型

要点——客户端不知道注入的服务类型

此模式的优点

类似模式

依赖反转原则——DIP(**)

控制反转——IoC(****)

依赖关系注入容器(****)

什么是DI容器

DI容器函数

DI容器:自动——简单示例

DI容器:自动——深度依赖示例

DI容器:Autofac——配置对象作用域和生存期

DI容器——与应用程序框架集成

结论

引用


面试求生教程

本文的目标是提供有关依赖项注入模式和相关主题的简短教程。对于那些想要了解该主题的人来说,它可以用作第一次联系教程,也可以用作那些想要更新知识的人的复习材料。它可以作为那些需要快速掌握基本概念的人的面试准备材料。

本教程中涵盖的主题通常会要求面试高级(.NET)开发人员职位的候选人。

这是一个基本教程,不涉及所有细节。人们在同心圆中呈现知识时学习得最好,在第一个圆中,他们被教导一切的基础,在第二个同心圆中,他们回顾他们在前一个圆圈中学到的知识,并用更多细节扩展该知识,然后在下一个圆圈中他们再次做类似的事情,等等。本文旨在为感兴趣的读者提供该主题的第一遍。

提出的主题

介绍以下主题:

  • 依赖关系注入模式——DI(*)
  • 依赖反转原理——DIP(**)
  • 控制反转——IoC(****)
  • 依赖关系注入容器(****)

依赖关系注入模式——DI*

首先,依赖注入模式是一种软件设计模式。它被称为模式,因为它建议对特定问题进行低级的特定实现。

此模式旨在解决的主要问题是如何创建松散耦合组件。它通过将组件的创建与其依赖项分开来实现这一点。

此模式中有四个主要角色(类):

  1. Client:客户端是一个组件/类,它想要使用另一个组件提供的服务,称为Service
  2. Service-Interface:服务接口是描述服务组件提供的服务类型的抽象。
  3. ServiceService组件/类正在根据服务接口描述提供服务。
  4. Injector:是一个组件/类,其任务是创建客户端和服务组件并将它们组合在一起。

它的工作方式是客户端依赖于服务接口IService。客户端依赖于IService接口,但不依赖于服务本身。服务IService实现接口并提供客户端需要的某些服务。注入器创建ClientService并将它们组装在一起。我们说注入器将服务注入到客户端。

下面是此模式的类图:

下面是此模式的示例代码:

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]中,人们可以发现他们提到了不同类型的依赖注入,根据将服务注入客户端的方法进行分类。我认为这不是一个重要的区别,因为效果总是相同的,即无论如何,对服务的引用都会传递给客户。但是,为了完整起见,让我们解释一下。

因此,依赖注入的类型是:

  1. 构造函数注入——注入是在Client构造函数中完成的
  2. 方法注射——通过专用方法完成注射
  3. 属性注入——通过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不知道正在注入哪个服务,如果是Service1Service2或者Service3。这就是想要的,我们看到组件/Client, Service1, Service2,Service3松散耦合的

Client类现在更具可重用性和可测试性。此功能的一个典型用法是,在生产环境中,Client注入了真实的Service1服务,而在测试中,环境Client注入了Service2,这是一个专为测试而创建的模拟服务。

此模式的优点

此模式的好处是:

  • 创建松散耦合的组件/类ClientService
  • 客户端对服务没有依赖性,也不了解,这使得它更可重用和可测试。
  • 支持不同开发人员/团队并行开发组件/类客户端和服务,因为它们之间的边界由IService接口明确定义
  • 它简化了组件的单元测试

这种模式带来的缺点是:

  • 在规划、创建和维护接口方面投入更多精力
  • 依赖注入器来组装组件/类

类似模式

这种模式与GoF书籍战略模式[2]非常相似。类图实际上是相同的。区别在于意图:

  1. 依赖注入更像是结构模式,其目的是组装松散耦合的组件,一旦组装,它们通常在客户端生命周期内保持这种状态;而
  2. 策略模式是行为模式,其目的是为问题提供不同的算法,这些算法通常在客户端生命周期内可以互换。

依赖反转原则——DIP**

因此,依赖反转原则(DIP是一个软件设计原则。它被称为原则,因为它提供了有关如何设计软件产品的高级建议。

DIPRobert C. Martin [5]提倡的首字母缩略词SOLID [3]下的五个设计原则之一。DIP原则规定:

  1. 高级模块不应依赖于低级模块。两者都应该依赖于抽象。
  2. 抽象不应依赖于细节。细节应该取决于抽象。

解释是:

虽然高级原则讨论的是抽象,但我们需要将其转换为特定编程环境(在本例中为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容器映射,以便它可以正确注入正确的类型。在这里,您将向容器提供工作所需的基本信息。

管理对象作用域和生存期。您需要告诉容器它创建的对象将具有什么范围和生存期。
典型的生命周期模式是:

  1. 单例:始终使用对象的单个实例。
  2. 瞬态:每次创建对象的新实例时。
  3. 范围:这通常是每个隐式或显式定义的范围的单一实例模式。

例如,您需要告诉容器是否希望在每次解析依赖项时创建一个新对象,或者是否希望应用单例模式。例如,单例可以是每个进程、每个线程或每个用户定义的范围。此外,还需要指定对象的所需生存期。例如,您可以将每个进程的对象生存期或对象生存期配置为每个用户定义的范围,这意味着对象将在用户定义的范围结束时释放。容器可以强制执行所有这些,只需要精确配置即可。

解析方法。以下是创建和组装所需对象/类型的实际工作。容器创建特定类型的对象,解析所有依赖项,并将其注入到创建的对象中。该方法以递归方式深入工作,直到解析所有依赖项。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模式(*)的术语中同时是ClientService。它是C类的Service,类UVClient。对象容器在(*)的术语中扮演Injector的角色

再次,一个简短的说明。这是一个教程概念代码演示。我们在上面的示例中使用 DI 容器的方式,通过在顶层显式请求依赖项解析,使其有点类似于服务定位器模式 [29]DI 容器的正确用法是将其用作框架,而不是显式请求解析依赖项。

DI容器:Autofac——配置对象作用域和生存期

如果我们看上面的例子,会出现一个问题。如果我们有两个C类的对象,对象c1c2,由DI容器通过解析生成,这些对象是不同还是相同?那么依赖对象呢,比如T类的对象,我们称它们为t1t2,它们都是不同的还是相同的?

//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);  

答案是:这是可配置的。但是由于我们没有在前面的示例中对其进行配置,因此我们将获得默认行为,即每次创建一个新对象。在这个例子中,对象c1c1是不同的,因为所有类STUVZX的依赖对象都是不同的。

DI容器Autofac[11][12])配置对象范围和生存期的典型选项包括:

  1. 每个依赖项的实例数:在文献中也经常称为“瞬态”。这是一种“每次新对象”模式。基本上,这意味着每次请求对象时,都会创建一个新实例。这是默认行为。
  2. 单个实例:也称为“单一实例”。这基本上是“每个进程的单例每次在请求解析的过程中,您都会获得对象的相同实例。
  3. 每个生存期范围的实例数:即“每个用户定义的单例范围”。用户需要指定范围,在其中,他将始终获得相同的实例。
  4. 每个匹配生存期范围的实例数:这又是单例,但这次是“每个用户定义的命名作用域的单例”。用户需要使用定义的命名范围,并且每次在其中时都会获得对象的相同实例。
  5. 每个请求的实例数:在ASP类型的应用程序中,它会导致“每个请求的单一实例”。这实际上与4相同,只是每个请求创建命名范围。更多解释见[12]。
  6. 每个拥有的实例:这有点复杂,所以我们不会在这里详细介绍。更多解释见[12]。
  7. 线程作用域:它不作为单独的配置选项存在,而是依赖于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]

引用

https://www.codeproject.com/Articles/5333947/Dependency-Injection-Pattern-in-Csharp-Short-Tutor

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐