一、控制反转是什么(IOC)

控制反转(Inversion of Control,英文缩写为 IOC)是框架的重要特征,是一种思想,是一个目标。它是一种全新的设计模式。

应用控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。所以,控制反转是,关于一个对象如何获取他所依赖的对象的引用,这个责任的反转。

即:将你设计好的对象交给容器控制,对象之间的依赖关系由容器来创建。而不是传统意义上在你的对象内部直接控制。换句话说,就是由容器控制程序之间的关系,而非传统实现中,由程序代码直接操控。

二、实现策略

依赖注入(Dependency Injection,英文缩写为 DI)是组件之间依赖关系由容器在运行期决定,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。在.net中只有依赖注入。

依赖查找(Dependency Lookup)是容器中的受控对象通过容器的API来查找自己所依赖的资源和协作对象。这种方式虽然降低了对象间的依赖,但是同时也使用到了容器的API,造成了我们无法在容器外使用和测试对象。 依赖查找是一种更加传统的IoC实现方式。

三、IOC的优缺点

优点:IOC最大的好处是什么?因为把对象生成放在了XML里定义,所以当我们需要换一个实现子类将会变成很简单(一般这样的对象都是实现于某种接口的),只要修改XML就可以了。

  • 1、简化了对象的创建,特别是针对分层对象结构和依赖关系;
  • 2、需求的抽象,允许开发人员在运行时或配置文件中指定依赖关系,简化横切关注点的管理;
  • 3、推迟为容器配置组件的时机,增加了灵活性;
  • 4、服务定位能力,这使客户能够存储或缓存容器;
  • 5、实例和类型拦截

缺点

  • 1、生成一个对象的步骤变复杂了(事实上操作上还是挺简单的),对于不习惯这种方式的人,会觉得有些别扭和不直观。
  • 2、对象生成因为是使用反射编程,在效率上有些损耗。但相对于IOC提高的维护性和灵活性来说,这点损耗是微不足道的,除非某对象的生成对效率要求特别高。
  • 3、缺少IDE(集成开发环境)重构操作的支持,如果在Visual Studio要对类改名,那么你还需要去XML文件里手工去改了,这似乎是所有XML方式的缺陷所在。

四、IOC容器的应用

一、Unity容器:

1. 程序中安装Unity

使用管理NuGet程序包来安装Unity,在项目上右键,选择管理NuGet程序包:
在这里插入图片描述
在搜索框里面输入Unity,点击右侧安装按钮进行安装:
在这里插入图片描述

2. 使用Unity实现DI

首先,定义三个接口类,分别为 IPhone、IHeadPhone、IPower

接口 IPhone 类:

namespace IOC.Interface
{
    public interface IPhone
    {
        void Call();
        void Text();

        IHeadphone iHeadphone { get; set; }
    }
}

接口 IHeadPhone 类:

namespace IOC.Interface
{
    public interface IHeadphone
    {
    }
}

接口 IPower 类:

namespace IOC.Interface
{
    public interface IPower
    {
    }
}

然后,定义两个接口实现类,分别为 AndroidPhone 和 ApplePhone

实现 AndroidPhone 类:

using IOC.Interface;
using System;

namespace IOC.Service
{
    public class AndroidPhone : IPhone
    {
        public IHeadphone iHeadphone { 
        	get => throw new NotImplementedException(); 
        	set => throw new NotImplementedException(); 
        }
        public IPower iPower { 
        	get => throw new NotImplementedException(); 
        	set => throw new NotImplementedException(); 
        }

        public AndroidPhone()
        {
            System.Diagnostics.Debug.WriteLine($"{this.GetType().Name}构造函数");
        }

        public void Call()
        {
            System.Diagnostics.Debug.WriteLine($"{this.GetType().Name}打电话");
        }
    }
}

实现 ApplePhone 类:

using IOC.Interface;
using System;

namespace IOC.Service
{
    public class ApplePhone : IPhone
    {
    	[Dependency]  //属性注入
        public IHeadphone iHeadphone { get; set; }

        public ApplePhone()
        {
            System.Diagnostics.Debug.WriteLine($"{this.GetType().Name}构造函数");
        }
        
		[InjectionConstructor]  //构造函数注入:最好这样,默认找参数最多的构造函数
        public ApplePhone(IHeadphone headphone)
        {
            this.iHeadphone = headphone;
            System.Diagnostics.Debug.WriteLine($"{this.GetType().Name}带参数构造函数");
        }

        public void Call()
        {
            System.Diagnostics.Debug.WriteLine($"{this.GetType().Name}打电话");
        }
        
        [InjectionMethod]  //方法注入:最好不要,增加一个没有意义的方法,破坏封装
        public void Init(IPower power)
        {
            this.iPower = power;
        }
    }
}

最后,再进行调用测试,此处为 MVC 框架的简单模型,写在 HomeControler 控制器下的方法,如下:

using IOC.Interface;
using IOC.Service;
using System.Web.Mvc;
using Unity;public ActionResult UnityTest_one()
	{
	    IUnityContainer container = new UnityContainer();  //定义一个容器
	    container.RegisterType<IPhone, AndroidPhone>();  //注册类型,表示遇到IPhone类型,创建AndroidPhone的实例
	    IPhone phone = container.Resolve<IPhone>();  //创建实例
	    phone.Call();	//调用实例方法
	    return Content("");
	}

执行 http://localhost:57081/home/unitytest_one 结果:
在这里插入图片描述

3. 三种注入方式

三种注入方式:

  • 构造函数注入
  • 属性注入
  • 方法注入

如定义的 IHeadphone 接口实现类 Headphone

using IOC.Interface;
using System;

namespace IOC.Service
{
    public class Headphone : IHeadphone
    {
        public Headphone()
        {
            System.Diagnostics.Debug.WriteLine("Headphone 被构造");
        }
    }
}

和 IPower 接口实现类 Power

using IOC.Interface;
using System;

namespace IOC.Service
{
    public class Power : IPower
    {
        public Power()
        {
			System.Diagnostics.Debug.WriteLine("Power 被构造");
        }
    }
}

调用测试:

using IOC.Interface;
using IOC.Service;
using System.Web.Mvc;
using Unity;public ActionResult UnityTest_two()
	{
		IUnityContainer container = new UnityContainer();
		container.RegisterType<IPhone, ApplePhone>();
	    //container.RegisterType<IMicrophone, Microphone>(); //注释行
		container.RegisterType<IHeadphone, Headphone>();
		container.RegisterType<IPower, Power>();
	    IPhone phone = container.Resolve<IPhone>();
		return Content("");
	}

container.RegisterType<IMicrophone, Microphone>();

  • 在此处只是另外一套 IHeadphone 和 Headphone 的复制版,并将 [Dependency] 置于iMicrophone属性上,是为了更好的说明 “Headphone 被构造” 和 “Microphone 被构造”的先后顺序,此文只是简化了这一道工序,区别处如下图,见谅~~
    在这里插入图片描述

执行 http://localhost:57081/home/unitytest_two 结果:
在这里插入图片描述
从输出结果中可以看出三种注入方式的执行顺序:先执行构造函数注入,在执行属性注入,最后执行方法注入。

4. 一个接口实现多个注册

同一个接口,不同实例的创建,一个 IPhone 接口有两个实现类 ApplePhone 和 AndroidPhone,在注册时,我们可以传入一个不同的参数以示区别,在调用中只传递注册时标识的参数就能识别,具体代码如下:

using IOC.Interface;
using IOC.Service;
using System.Web.Mvc;
using Unity;public ActionResult UnityTest_three()
	{
	    IUnityContainer container = new UnityContainer();
	    container.RegisterType<IPhone, ApplePhone>();	//注册IPhone的第一个实现类
	    container.RegisterType<IPhone, AndroidPhone>("Android");//Iphone的第二个实现类
	    //container.RegisterType<IMicrophone, Microphone>();
	    container.RegisterType<IHeadphone, Headphone>();
	    container.RegisterType<IPower, Power>();
	    IPhone phone = container.Resolve<IPhone>();		//构造了ApplePhone
	    IPhone Android = container.Resolve<IPhone>("Android");	//构造了AndroidPhone
	    return Content("");
	}

执行 http://localhost:57081/home/unitytest_three 结果,区别在于红色箭头所指:
在这里插入图片描述
通过结果可以看出,被注册的 IPhone 接口类型默认是构造 ApplePhone,如果想构造 AndroidPhone,那就得传递一个 “Android” 参数以示区别。

5. 对象的生命周期

通过在注册接口实现的时候传入以下参数

  • new TransientLifetimeManager(),则实现创建多个对象。
  • new ContainerControlledLifetimeManager(),即可实现容器单例。
  • new PerThreadLifetimeManager(),线程

代码如下:

using IOC.Interface;
using IOC.Service;
using System.Web.Mvc;
using Unity;
using Unity.Lifetime;public ActionResult UnityTest_four()
	{
	    IUnityContainer container = new UnityContainer();
	    container.RegisterType<IPhone, AndroidPhone>();
	    container.RegisterType<IPhone, AndroidPhone>(new TransientLifetimeManager());//每一次都是全新生成
	    container.RegisterType<IPhone, AndroidPhone>(new ContainerControlledLifetimeManager());//容器单例  单例就不要自己实现
	    var phone1 = container.Resolve<IPhone>();
	    var phone2 = container.Resolve<IPhone>();
	    System.Diagnostics.Debug.WriteLine($"object.ReferenceEquals(phone1, phone2) = {object.ReferenceEquals(phone1, phone2)}");

		container.RegisterType<IPhone, AndroidPhone>(new PerThreadLifetimeManager());
        //线程单例:相同线程的实例相同 不同线程的实例不同   web请求/多线程操作
        IPhone iphone1 = null;
        Action act1 = new Action(() =>
        {
            iphone1 = container.Resolve<IPhone>();
            System.Diagnostics.Debug.WriteLine($"iphone1由线程id={Thread.CurrentThread.ManagedThreadId}");
        });
        var result1 = act1.BeginInvoke(null, null);

        IPhone iphone2 = null;
        Action act2 = new Action(() =>
        {
            iphone2 = container.Resolve<IPhone>();
            System.Diagnostics.Debug.WriteLine($"iphone2由线程id={Thread.CurrentThread.ManagedThreadId}");
        });

        IPhone iphone3 = null;
        var result2 = act2.BeginInvoke(t =>
        {
            iphone3 = container.Resolve<IPhone>();
            System.Diagnostics.Debug.WriteLine($"iphone3由线程id={Thread.CurrentThread.ManagedThreadId}");
            System.Diagnostics.Debug.WriteLine($"object.ReferenceEquals(iphone2, iphone3) = {object.ReferenceEquals(iphone2, iphone3)}");
        }, null);

        act1.EndInvoke(result1);
        act2.EndInvoke(result2);

        System.Diagnostics.Debug.WriteLine($"object.ReferenceEquals(iphone1, iphone2) = {object.ReferenceEquals(iphone1, iphone2)}");
	    return Content("");
	}

执行 http://localhost:57081/home/unitytest_four 结果如下:
在这里插入图片描述
由执行结果可以看出,phone1 和 phone2 为同一对象,只构造了一次 AndroidPhone。iphone1 和 iphone2 在不同线程,创建的不是同一个对象,分别构造了 AndroidPhone。而 iphone2 和 iphone3 在同一线程内,创建对象时是同一对象。

由此可以说明 PerThreadLifetimeManager 在同一线程内的对象是单一的,而不同线程之间是不共享对象的。

6. 细节依赖处理

以上代码都是依赖了细节,那么怎么才能实现不依赖于细节呢?那就需要使用 config 文件来配置信息,简单的一个配置文件 Unity.Config 可以参考以下代码:

<configuration>
  <configSections>
    <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Unity.Configuration"/>
  </configSections>
  <unity>
    <sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Unity.Interception.Configuration"/>
    <containers>

      <container name="ContainerOne"><!-- type属性是接口名、命名空间;mapto里是类名、命名空间 -->
        <register type="IOC.Interface.IPhone, IOC.Interface" mapTo="IOC.Service.ApplePhone, IOC.Service"/>
        <register type="IOC.Interface.IPhone, IOC.Interface" mapTo="IOC.Service.AndroidPhone, IOC.Service" name="Android"/>
        <!-- IMicrophone在该文中未介绍,可以理解为和 IHeadphone 与 Headphone 一样就行 -->
        <register type="IOC.Interface.IMicrophone, IOC.Interface" mapTo="IOC.Service.Microphone, IOC.Service"/>
        <register type="IOC.Interface.IHeadphone, IOC.Interface" mapTo="IOC.Service.Headphone, IOC.Service"/>
        <register type="IOC.Interface.IPower, IOC.Interface" mapTo="IOC.Service.Power, IOC.Service"/>
      </container>

    </containers>
  </unity>
</configuration>

特别注意:文件属性中需要设置【 复制到输出目录始终复制 】,这样程序在编译的时候该文件就会生成到Debug目录下。

另外,调用配置文件信息的方法可以参考以下代码:

using IOC.Interface;
using Microsoft.Practices.Unity.Configuration;
using System;
using System.Configuration;
using System.IO;
using System.Web.Mvc;
using Unity;public ActionResult UnityTest_five()
	{
	    ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
	    fileMap.ExeConfigFilename = Path.Combine(AppDomain.CurrentDomain.BaseDirectory + "Configs\\Unity.Config");//找配置文件的路径
	    Configuration configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
	    UnityConfigurationSection section = (UnityConfigurationSection)configuration.GetSection(UnityConfigurationSection.SectionName);
	
	    IUnityContainer container = new UnityContainer();
	    section.Configure(container, "ContainerOne");
	    IPhone phone = container.Resolve<IPhone>();
	    phone.Call();
	
	    IPhone android = container.Resolve<IPhone>("Android");
	    android.Call();
	
	    return Content("");
	}

执行 http://localhost:57081/home/unitytest_five 结果:
在这里插入图片描述由此可见,通过配置文件来实现对象的创建,只依赖于接口程序集,程序中没有具体实现的引用,去除了对细节的依赖。

7. 工厂模式处理细节依赖

既然可以通过配置文件来记录接口和实现的对应关系,那么,在每次对象的创建过程中,都要用到以上的代码,就会非常的繁琐,因此,需要将代码进行提取,将通用部分提取到单独的一个类中,每次创建对象就调用这个类。因此,我们在解决方案下创建一个类库,命名为 IOC.Framework,在该类库下新建文件夹名为IOC,在IOC下创建一个类 IOCFactory,代码如下:

using Microsoft.Practices.Unity.Configuration;
using System;
using System.Configuration;
using System.IO;
using Unity;

namespace IOC.Framework.IOC
{
    /// <summary>
    /// IOC容器工厂
    /// </summary>
    public class IOCFactory
    {
        private static IUnityContainer container;

        public IOCFactory() { }

        /// <summary>
        /// Unity注入容器
        /// </summary>
        /// <returns></returns>
        public static IUnityContainer UnityResolve()
        {
            ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
            fileMap.ExeConfigFilename = Path.Combine(AppDomain.CurrentDomain.BaseDirectory + "Configs/Unity.config");
            Configuration configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
            UnityConfigurationSection section = (UnityConfigurationSection)configuration.GetSection(UnityConfigurationSection.SectionName);

            container = new UnityContainer();
            section.Configure(container, "ContainerOne");   //方式一,选其一
            container.LoadConfiguration(section, "ContainerOne");   //方式二,选其一

            return container;
        }
    }
}

调用代码如下:

using IOC.Framework.IOC;
using IOC.Interface;
using System.Web.Mvc;
using Unity;public ActionResult UnityContainer()
	{
	    IUnityContainer container = IOCFactory.UnityResolve();
		
	    IPhone phone = container.Resolve<IPhone>();
	    phone.Call();
		
	    IPhone android = container.Resolve<IPhone>("Android");
	    android.Call();
		
	    return Content("");
	}

运行 http://localhost:57081/Home/UnityContainer 结果:
在这里插入图片描述
这样我们就对Unity容器进行了封装操作,后面创建对象就简单了。只需要通过IOCFactory工厂的构造就能轻松调用方法。

二、AutoFac容器:

AutoFac和Unity容器的用法相当
步骤都类似,创建容器 > 注册接口实现类 > 创建接口容器 > 创建实例 > 调用实例方法,初步模型代码如下:

1、初实现
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<ApplePhone>().As<IPhone>(); //注册ApplePhone是接口IPhone的实现类
IContainer container = builder.Build();
IPhone phone = container.Resolve<IPhone>();
phone.Call();	//调用实例方法
2、演化去除接口类

以上是一个最为基础的创建使用方式,还可以将注册接口实现换一种写法,代码如下:

ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<ApplePhone>().AsImplementedInterfaces(); //注册ApplePhone实现类
IContainer container = builder.Build();
IPhone phone = container.Resolve<IPhone>();
phone.Call();	//调用实例方法
3、程序集加载

这第二种方式,接口实现类一旦多了之后,注册实现类同样比较的麻烦,于是再演化,代码如下:

ContainerBuilder builder = new ContainerBuilder();
Assembly asm = Assembly.Load("IOC.Service"); //实现类的程序集名称(不是接口类)
builder.RegisterAssemblyTypes(asm).AsImplementedInterfaces(); //把所有接口实现都注册一遍,由此,解除了程序的耦合
IContainer container = builder.Build();
IPhone phone = container.Resolve<IPhone>();
phone.Call();	//调用实例方法
4、程序集加载,单接口,多实现类

由于程序集内的 IPhone 接口有多个实现类,在第三点中调用会无法控制,有可能会调用 ApplePhone 类,也有可能调用 AndroidPhone 类,那我们就可以通过以下方式来获取多个实现类的对象,代码如下:

ContainerBuilder builder = new ContainerBuilder();
Assembly asm = Assembly.Load("IOC.Service"); //实现类的程序集名称(不是接口类)
builder.RegisterAssemblyTypes(asm).AsImplementedInterfaces(); //把所有接口实现都注册一遍,由此,解除了程序的耦合
IContainer container = builder.Build();

IEnumerable<IPhone> phones = container.Resolve<IEnumerable<IPhone>>();
foreach(IPhone Callphone in phones)
{
	System.Diagnostics.Debug.WriteLine(Callphone.GetType());
	Callphone.Call();	//两个实现类都会执行
}
5、实现类调用其他实现类方法,实现属性注入

多个方法一并查找出来,针对业务所需来进行判断调用。这时要是实现类内部想要调用其他接口的实现类,那只需在注册接口实现时再添加一个 .PropertiesAutowired() 就行,代码如下:

ContainerBuilder builder = new ContainerBuilder();
Assembly asm = Assembly.Load("IOC.Service"); //实现类的程序集名称(不是接口类)
builder.RegisterAssemblyTypes(asm).AsImplementedInterfaces()
.PropertiesAutowired(); 	//增加属性注入
IContainer container = builder.Build();
IPhone phone = container.Resolve<IPhone>();
phone.Call();	//调用实例方法
6、对象的生命周期

container多次创建相同对象时可以使用单例模式,在注册注册接口实现时加上 SingleInstance(),

ContainerBuilder builder = new ContainerBuilder();
Assembly asm = Assembly.Load("IOC.Service"); //实现类的程序集名称(不是接口类)
builder.RegisterAssemblyTypes(asm).AsImplementedInterfaces()
.PropertiesAutowired().SingleInstance(); 	//注册成单例模式
IContainer container = builder.Build();
IPhone phone1 = container.Resolve<IPhone>();
IPhone phone2 = container.Resolve<IPhone>();
System.Diagnostics.Debug.WriteLine(object.ReferenceEquals(phone1, phone2));

这样就能在每次引用的时候就都是同一个对象了。

关于 AutoFac 的具体使用可以参考哔站的 杨中科 老师公开视频,链接如下:

net开发工程师 基础+进阶(完)

文章中由很多细节处理的不太好,还请见谅,有技术原则性问题,还请您及时告知,切莫误人子弟。

Logo

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

更多推荐