设计模式のIOC(控制反转)
一、什么是Ioc
IoC(Inverse of Control)的字面意思是控制反转,它包括两个内容: 控制、反转
可以假设这样一个场景:火车运货,不同类型的车厢运送不同类型的货物,板车运送圆木,罐车运送柴油,箱车运送水果。那么对于运送货物这件事,需是列车挂不同的车厢运送货物。显然列车和运送货物之间是有依赖关系的(控制:依赖关系)。我们把列车挂什么样的车厢交给调度中心,而不是交给列车决定,这就形成了依赖反转。
因为IoC确实不够开门见山,因此业界曾进行了广泛的讨论,最终软件界的泰斗级人物Martin Fowler提出了DI(依赖注入:Dependency Injection)的概念用以代替IoC,即让调用类对某一接口实现类的依赖关系由第三方(容器或协作类)注入,以移除调用类对某一接口实现类的依赖。“依赖注入”这个名词显然比“控制反转”直接明了、易于理解。
依赖注入和控制反转是同一概念吗?
根据上面的讲述,应该能看出来,依赖注入和控制反转是对同一件事情的不同描述,从某个方面讲,就是它们描述的角度不同。依赖注入是从应用程序的角度在描述,可以把依赖注入描述完整点:应用程序依赖容器创建并注入它所需要的外部资源;而控制反转是从容器的角度在描述,描述完整点:容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源。
其实IoC/DI对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC/DI容器来创建并注入它所需要的资源了。
这么小小的一个改变其实是编程思想的一个大进步,这样就有效的分离了对象和它所需要的外部资源,使得它们松散耦合,有利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。
传统模式下是自己建立调用者对象,再去调用其他东西。而依赖注入是实现控制反转的方式,例如通过构造方法注入Service对象,从而建立了调用者(Service对象,相当于上面的斧头),而不是自己手动建立(Service的控制权在容器,而不在于自己)。
传统模式,例如自己new一个Service对象的话,控制器层的实例化肯定比Service层早。但是使用构造函数注入的话,Service层的实例化反而比控制器层早,也就是说Service层构造函数比controller层构造函数先执行
举个例子:有一个闹钟会问早安,当它地理位置定位在中国的时候,它会问候:早安;当在美国的时候,它会问候:Good Morning;那么我们把闹钟看做一个客户,问候看做一个服务,这个闹钟是依赖于服务的。闹钟遵从OCP原则应该有一个SayMorning的注入点,而问候就需要使用策略模式列出。实现这个功能:
public interface ISayMorning { void SayMorning(); } public class ChinesePosition:ISayMorning { void SayMorning() { Console.Writeline("早安!"); } } public class EnglishPosition:ISayMorning { void SayMorning() { Console.Writeline("GoodMorning!"); } } //下面创建客户 public class ClockClient() { public ISayMorning SayService{set;} public Set_Sayservices(ISayMorning sayService) { SayService=sayService; } public SayMorning() { SayService.SayMorning(); } } //主函数中 var clock=new ClockClient(); var saysChinese=new ChinesePosition(); clock.Set_Sayservices(saysChinese); clock.SayMorning();
以上,闹钟说话依赖于说话服务,说话服务有很多策略(算法),我们将服务注入(DL)客户,将客户的依赖项(服务)反转到对象创建后再根据类别注入选择,这就形成了依赖反转(IOC)。这样做的的好处是策略变化(地区),我们只要新建类就好了
而不用修改已经写好的代码,实现了OCP(设计模式遵循的六大原则之 开闭原则)。
二、几个相似相关的概念
依赖倒置原则(DIP):一种软件架构设计的原则(抽象概念)。
控制反转(IoC):一种反转流、依赖和接口的方式(DIP的具体实现方式)。
依赖注入(DI):IoC的一种实现方式,用来反转依赖(IoC的具体实现方式)。
IoC容器:依赖注入的框架,用来映射依赖,管理对象创建和生存周期(DI框架)。
三、Ioc的类型
IOC的实现有三种方式
构造注入:就是将开头例子中的Setter方式,在创建客户对象的时候,初始化进入。
Setter注入:就是开头的例子。
依赖获取。就是在注入的时候,利用一下虚拟工厂(Abstract Factory),这种适用于服务不仅一种的时候,比如我们赋予闹钟报时功能。
1、构造函数注入
public Class SayHello { private IPeople _people; public SayHello(IPeople p) { _people=p; } public void Say() { _people.Say(); } }
2、属性注入
using Microsoft.VisualStudio.TestTools.UnitTesting; using VisionLogic.Training.DependencyInjection.Scenario; namespace VisionLogic.Training.DependencyInjection.Scenario.UnitTest { [TestClass] public class SetterInjectionTest { class Client { private IWeatherReader reader; public IWeatherReader Reader { get { return reader; } set { reader = value; } } } [TestMethod] public void Test() { IWeatherReader reader = new Assembler<IWeatherReader>().Create(); Client client = new Client(); client.Reader = reader; Assert.IsNotNull(client.Reader); } } }
也可以写一个Setter方法
3、接口注入
using Microsoft.VisualStudio.TestTools.UnitTesting; using VisionLogic.Training.DependencyInjection.Scenario; namespace VisionLogic.Training.DependencyInjection.Scenario.UnitTest { [TestClass] public class InterfaceInjectionTest { interface IClientWithWeatherReader { IWeatherReader Reader { get; set;} } class Client : IClientWithWeatherReader { private IWeatherReader reader; #region IClientWithWeatherReader Members public IWeatherReader Reader { get { return reader; } set { reader = value; } } #endregion } [TestMethod] public void Test() { IWeatherReader reader = new Assembler<IWeatherReader>().Create(); Client client = new Client(); IClientWithWeatherReader clientWithReader = client; clientWithReader.Reader = reader; Assert.IsNotNull(clientWithReader.Reader); } } }
4、依赖获取
public interface ISayMorning { void SayMorning(); } public class ChinesePosition:ISayMorning { void SayMorning() { Console.Writeline("早安!"); } } public class EnglishPosition:ISayMorning { void SayMorning() { Console.Writeline("GoodMorning!"); } } public interface IFactory { ISayTime MakeTimeSayer(); ISayMoring MakeMorningSayer(); } public class FactoryAmerican:IFactory { public ISayTime MakeTimeSayer() { return new EnglishPositionTime(); //未实现 } public ISayMoring MakeMorningSayer() { return new EnglishPosition(); } } //位于中国的工厂 public class FactoryChinese:IFactory { public ISayTime MakeTimeSayer() { return new ChinesePositionTime(); //未实现 } public ISayMoring MakeMorningSayer() { return new ChinesePosition(); } } public statics class FactoryContainer { static FactoryContainer() { XmlDocument xmlDoc = new XmlDocument(); xmlDoc.Load("Config.xml"); XmlNode xmlNode =xmlDoc.ChildNodes[1].ChildNodes[0].ChildNodes[0]; if ("Chinese" == xmlNode.Value) { factory = new FactoryChinese(); } else if ("American" == xmlNode.Value) { factory = new FactoryAmerican(); } else { throw new Exception("Factory Init Error"); } } } } //调用 IFactory factory = FactoryContainer.factory; IWindow window = factory.SayMorning();
这样,我们可以用过xml文件配置工厂(也就是不同的定位条件)。
四、一个例子
C# using System; namespace VisionLogic.Training.DependencyInjection.Scenario.Attributer { /// <summary> /// 抽象的处理对象 /// </summary> public interface IObjectWithGuid { string Guid { get; set;} } } 定义需要注入的限制接口,并用一个Attribute管理它 C# using System; namespace VisionLogic.Training.DependencyInjection.Scenario.Attributer { /// <summary> /// 需要注入的用以限制最大数量的接口 /// </summary> public interface ICapacityConstraint { int Max { get;} } public class CapacityConstraint : ICapacityConstraint { private int max; public CapacityConstraint(){this.max = 0;} // 默认情况下不限制 public CapacityConstraint(int max) { this.max = max; } public int Max { get { return max; } } } [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class ConstraintAttribute : Attribute { private ICapacityConstraint capacity; public ConstraintAttribute(int max) { this.capacity = new CapacityConstraint(max); } public ConstraintAttribute() { this.capacity = null; } public ICapacityConstraint Capacity { get { return capacity; } } } } Assembler上增加通过Attribute注入限制的响应。 using System; using System.Collections.Generic; namespace VisionLogic.Training.DependencyInjection.Scenario.Attributer { public class Assembler { /// <summary> /// 登记相关类型对“最大容量”属性的使用情况 /// </summary> private IDictionary<Type, ConstraintAttribute> attributeRegistry = new Dictionary<Type, ConstraintAttribute>(); /// <summary> /// 登记每个类型(如须受到“最大容量”属性限制的话),实际已经创建的对象数量 /// </summary> private IDictionary<Type, int> usageRegistry = new Dictionary<Type, int>(); public T Create<T>() where T : IObjectWithGuid, new() { ICapacityConstraint constraint = GetAttributeDefinedMax(typeof(T)); if ((constraint == null) || (constraint.Max <= 0)) // max <= 0 代表是不需要限制数量的。 return InternalCreate<T>(); else { if (usageRegistry[typeof(T)] < constraint.Max) // 检查是否超出容量限制 { usageRegistry[typeof(T)]++; // 更新使用情况注册信息 return InternalCreate<T>(); } else return default(T); } } // helper method // 直接生成特定实例,并setter 方式注入其guid。 private T InternalCreate<T>() where T : IObjectWithGuid, new() { T result = new T(); result.Guid = Guid.NewGuid().ToString(); return result; } /// helper method. // 获取特定类型所定义的最大数量, 同时视情况维护attributeRegistry 和usageRegistry 的注册信息。 private ICapacityConstraint GetAttributeDefinedMax(Type type) { ConstraintAttribute attribute = null; if (!attributeRegistry.TryGetValue(type, out attribute)) //新的待创建的类型 { // 填充相关类型的“最大容量”属性注册信息 object[] attributes = type.GetCustomAttributes(typeof(ConstraintAttribute), false); if ((attributes == null) || (attributes.Length <= 0)) attributeRegistry.Add(type, null); else { attribute = (ConstraintAttribute)attributes[0]; attributeRegistry.Add(type, attribute); usageRegistry.Add(type, 0); // 同时补充该类型的使用情况注册信息 } } if (attribute == null) return null; else return attribute.Capacity; } } }
4.2对方案的测试
using Microsoft.VisualStudio.TestTools.UnitTesting; using VisionLogic.Training.DependencyInjection.Scenario.Attributer; namespace VisionLogic.Training.DependencyInjection.Scenario.UnitTest.Attributer { [TestClass()] public class AssemblerTest { public abstract class ObjectWithGuidBase : IObjectWithGuid { protected string guid; public virtual string Guid { get { return guid; } set { guid = value; } } } [Constraint(2)] // 通过属性注入限制 public class ObjectWithGuidImplA : ObjectWithGuidBase { } [Constraint(0)] // 通过属性注入限制 public class ObjectWithGuidImplB : ObjectWithGuidBase { } [Constraint(-5)] // 通过属性注入限制 public class ObjectWithGuidImplC : ObjectWithGuidBase { } public class ObjectWithGuidImplD : ObjectWithGuidBase { } [TestMethod] public void Test() { Assembler assembler = new Assembler(); for (int i = 0; i < 2; i++) Assert.IsNotNull(assembler.Create<ObjectWithGuidImplA>()); Assert.IsNull(assembler.Create<ObjectWithGuidImplA>()); // 最多两个 for (int i = 0; i < 100; i++) Assert.IsNotNull(assembler.Create<ObjectWithGuidImplB>()); // 不限制 for (int i = 0; i < 100; i++) Assert.IsNotNull(assembler.Create<ObjectWithGuidImplC>()); // 不限制 for (int i = 0; i < 100; i++) Assert.IsNotNull(assembler.Create<ObjectWithGuidImplD>()); // 不限制 } } }
五、依赖注入的生命周期
Transient:服务级别,每次访问Service的时候,就会产生一个新的对象实例。
Scoped: 会话级别(http级别),每次新的http请求时,就会产生一个新的对象实例,同一个http请求,才会复用该相同实例。
Singleton:单例服务,单一实例对象对每个对象和每个请求都是相同的,可以说是不同客户端不同请求都是相同的。
a、如果使用的是Transient,则图中3个Service的对象的实例都不相同,每个都不一样 ,因为每调用一次Service都会新建一个实例。
b、如果是Scoped,则每个接口就是一个Scoped会话,该接口复用同一个实例。
c、如果是Singleton,则该Controller里面的接口中的Service调用的实例都是一致的。
六、常用的依赖注入
一、构造器注入
将被依赖对象通过构造函数的参数注入给依赖对象,并且在初始化对象的时候注入。
优点:
对象初始化完成后便可获得可使用的对象。
缺点:
当需要注入的对象很多时,构造器参数列表将会很长;
不够灵活。若有多种注入方式,每种方式只需注入指定几个依赖,那么就需要提供多个重载的构造函
数,麻烦。
二、setter 方法注入
IoC Service Provider 通过调用成员变量提供的 setter 函数将被依赖对象注入给依赖类。
优点:
灵活。可以选择性地注入需要的对象。
缺点:
依赖对象初始化完成后由于尚未注入被依赖对象,因此还不能使用。
三、接口注入
依赖类必须要实现指定的接口,然后实现该接口中的一个函数,该函数就是用于依赖注入。该函数的参
数就是要注入的对象。
优点
接口注入中,接口的名字、函数的名字都不重要,只要保证函数的参数是要注入的对象类型即可。
缺点:
侵入行太强,不建议使用。