控制反转

控制反转是一种思想,其含义是将对象的创建转移至框架上,或者说是当我们需要用到某个类的实例时,我们只需告诉框架我们所需要的实例类型,框架就会自动提供我们相应类型的实例,当然至于这个框架是怎么创建出这个实例的,我们并不需要知道。

using System;
using System.Collections.Generic;

namespace Test
{
    public class Test
    {
        public static void Main(string[] Args)
        {
            InstanceProvider Provider = new InstanceProvider(Initialise);
            object Instance = Provider.GetInstance("Test.Test");
        }//void Main(string[] Args)

        public static void Initialise(Dictionary<string, object> InstanceDictionary)
        {
            InstanceDictionary.Add("Test.Test", new Test());
        }//void Initialise(Dictionary<string, object> InstanceDictionary)

    }//class Test

    public class InstanceProvider
    {
        /// <summary>
        /// 若传入的委托不为null,则调用该委托,并传入该InstanceProvider内部的InstanceDictionary属性,从而注册所需的实例
        /// </summary>
        /// <param name="Initialise"></param>
        public InstanceProvider(Action<Dictionary<string, object>> Initialise)
        {
            Initialise?.Invoke(InstanceDictionary);
        }//InstanceProvider(Action<Dictionary<string, object>> Initialise)

        /// <summary>
        /// 这是一个键值对集合,键为string,表示的是类型名称,值为object类型,表示的是一个实例
        /// </summary>
        private Dictionary<string, object> InstanceDictionary { get; } = new Dictionary<string, object>();

        /// <summary>
        /// 根据所需类型的名称获取相应的实例
        /// </summary>
        /// <param name="TypeName"></param>
        /// <returns></returns>
        public object GetInstance(string TypeName)
        {
            return InstanceDictionary[TypeName];
        }//object GetInstance(string TypeName)

    }//class InstanceProvider

}//namespace Test

哪怕是像上面这样,在程序的最开始,将所需的类型实例全部放在一个键值对集合中,等需要时通过注册时所用的类型名称再取出来,这也完全可以说是对控制反转这一思想的一种实现。备注:当然没有哪个框架会是这么得简单,这儿旨在说明控制反转这一思想的核心。

依赖注入

什么是依赖

public class Dependency
{
    private string HelloWorld { get; set; } = "Hello World";

    public void WriteLine()
    {
        //在控制台中输出HelloWorld属性的值
        Console.WriteLine(HelloWorld);
    }//void WriteLine()

}//class Dependency

像上面这段代码,在 Dependency 中有一个 HelloWorld 属性,同时还有一个 WriteLine 方法。当我们创建出一个 Dependency 实例,并调用其 WriteLine 方法时,在控制台中输出的必然是 “Hello World",绝不可能是其他。

那么问题来了,假如我要求你在不改变 Dependency 内部代码的情况下,调用 Dependency 实例中的 WriteLine 方法,使得控制台输出除” Hello World“以外的字符串,这又应该如何实现?(备注:也不能用反射及其他非正常的方式)

很明显不能,因为 string HelloWorld 属性是私有的,我们无法在 Dependency 外部改变该属性所对应字段的值,换句话说 Dependency 实例中的 WriteLine 方法依赖于 string HelloWorld 属性。这就是依赖的含义。

什么是依赖注入

依赖注入的本质就是从外部将依赖注入进对象之中。就那刚才那段代码来说,我们要向 Dependency 中添加哪些方法,就能改变 Dependency 实例中 string HelloWorld 属性所对应字段的值,从而使得我们在调用 Dependency 实例中的 WriteLine 方法后,控制台输出除” Hello World“以外的字符串。

方法注入
public class Dependency
{
    private string HelloWorld = "Hello World";

    public void ChangeHelloWorld(string NewHelloWorld)
    {
        HelloWorld = NewHelloWorld;
    }//void ChangeHelloWorld(string NewHelloWorld)

    public void WriteLine()
    {
        //在控制台中输出HelloWorld属性的值
        Console.WriteLine(HelloWorld);
    }//void WriteLine()

}//class Dependency

例如向这样,我们可以在 Dependency 中添加一个 ChangeHelloWorld 的方法,其将传入一个 string NewHelloWorld,并将该 NewHelloWorld 赋值给 Dependency 内部的 string HelloWorld 所对应的字段,这样我们只需在调用 ChangeHelloWorld 的方法,改变 Dependency 内部的 HelloWorld 所对应的字段后,再调用 Dependency 实例中的 WriteLine 方法,就能实现控制台输出除” Hello World“以外的字符串。

像这种通过添加其他的方法来改变依赖的方式,我们叫做方法注入。

构造函数注入
public class Dependency
{
    public Dependency(string NewHelloWorld)
    {
        HelloWorld = NewHelloWorld;
    }//Dependency(string NewHelloWorld)

    private string HelloWorld { get; set; } = "Hello World";

    public void WriteLine()
    {
        //在控制台中输出HelloWorld字段的值
        Console.WriteLine(HelloWorld);
    }//void WriteLine()

}//class Dependency

除了在 Dependency 内部添加相应的方法来改变 string HelloWorld 所对应字段的值外,我们也可以在创建 Dependency 对象时,就传入一个 string NewHelloWorld,并将该 NewHelloWorld 赋值给 Dependency 内部的 string HelloWorld 所对应的字段。这样我们就能在创建 Dependency 对象时,就改变了 string HelloWorld 所对应字段的值,也就能实现在我们调用 WriteLine 方法后,控制台输出除” Hello World“以外的字符串。

像这种通过在构造函数中传入相应对象,并改变依赖的方式,我们叫做构造函数注入。

依赖注入

像上面说的方法注入、构造函数注入,这些都是依赖注入的不同的表现形式,当然还有其他的注入方式,但是究其本质就是改变对象内部的依赖。

控制反转 + 依赖注入

就个人认为,控制反转和依赖注入之间并没有多大的关系。(备注:可能我的理解和别人有些不太一样)

好比是我们去麦当劳点一份双层吉士汉堡套餐(双层吉士汉堡、中份薯条、中份可乐加冰,备注:不要和我说可以改饮料和薯条),那么我们通过麦当劳收到这个套餐,这就是控制反转,即套餐是由麦当劳制作,而不是我们自己,而双层吉士汉堡套餐中需要有一个双层吉士汉堡、一个中份薯条、一个中份可乐加冰,而这些所需的材料都放进套餐中就是依赖注入。

总的来说,控制反转旨在谁去创建对象,依赖注入旨在如何创建符合要求的对象。

生命周期

对于一个框架来说,其不止要实现控制反转和依赖注入的思想,同时也要实现对所生成对象的生命周期的管理。

因为对于一个程序来说,每次获取某类型的实例时,可能需要是同一个对象,也可能需要的是新的对象,更可能的是有时候需要同一个对象有时候需要新的对象,也因此对于框架来说,一般都会将生命周期分为 3 种。

就以微软 ASP.NET Core 中所默认使用的依赖注入框架来说,其将生命周期分为了 3 种。(备注:一般来说,其它同类型的框架也同样分为这么几种,只不过可能名称不同)

public enum ServiceLifetime
{
    Singleton,

    Scoped,

    Transient,
}

ServiceLifetime.Singleton 表示的是单例模式,即从任意一个 ServiceProvider 实例中提取到的同一类型的对象都是同一对象。

ServiceLifetime.Scoped 表示的是从同一个 ServiceProvider 实例中提取到的同一类型的对象都是同一对象。

ServiceLifetime.Transient 表示的是每次从 ServiceProvider 实例中提取同一类型的对象都会创建出一个新的对象实例。

备注:在其框架中,ServiceProvider 是一个实例对象,我们可以从其含有的 GetService 实例方法中提到到相应的对象。

释放资源

另外 ServiceProvider 还继承了 IDisposable 接口,我们可以通过调用其 Dispose 方法即可释放由该 ServiceProvider 实例对象所创建的对象。(感觉这么说有点不太对,不过算了,具体的要牵扯到这个框架的内部实现了,所以就不说了)

反射

虽然像 Java 的 Spring、.Net 的 Autofac 等框架中均使用了反射来实现依赖注入,但这并不是说要实现依赖注入就必须使用反射。

只不过对于一个框架来说,由于在静态语言中程序的运行期与编译期之间是严格分离,要想让程序在运行期干编译期的事情就必须借助于反射,因此使用反射就能大大地增加了框架/程序的灵活性,就能更好地动态生成对象。

总结

控制反转是一个框架的核心思想,依赖注入和生命周期(包括资源回收)是一个框架所实现的重要功能/需求,反射是一个框架的最重要的实现手段。


↙↙↙阅读原文可查看相关链接,并与作者交流