设计模式の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>()); // 不限制 
        } 
    } 
}
 
 目的:   引入依赖注入的目的是为了解耦。说白了就是面向接口编程,通过调用接口的方法,而不直接实例化对象去调用。这样做的好处就是如果添加了另一个实现类,不需要修改之前代码,只需要修改注入的地方将实现类替换。上面说的通过接口调用方法,实际上还是需要去实例化接口的实现类,只不过不需要我们手动new 构造实现类,而是交给如微软的DI、Autofac这些工具去构建实现类。我们只需要告诉它们,某个类是某个接口的实现类,当用到的时候,工具(比如,微软的DI)会自动通过构造函数实例化类。
 
 依赖注入是实现控制反转的一种设计方法,并不是说依赖注入等于控制反转(Inversion of Control,IoC)。控制反转是思想,依赖注入是具体实现方式。
 

五、依赖注入的生命周期

Transient:服务级别,每次访问Service的时候,就会产生一个新的对象实例。

Scoped: 会话级别(http级别),每次新的http请求时,就会产生一个新的对象实例,同一个http请求,才会复用该相同实例。

Singleton:单例服务,单一实例对象对每个对象和每个请求都是相同的,可以说是不同客户端不同请求都是相同的。

 

a、如果使用的是Transient,则图中3个Service的对象的实例都不相同,每个都不一样 ,因为每调用一次Service都会新建一个实例。         

b、如果是Scoped,则每个接口就是一个Scoped会话,该接口复用同一个实例。

c、如果是Singleton,则该Controller里面的接口中的Service调用的实例都是一致的。

 

六、常用的依赖注入

一、构造器注入
将被依赖对象通过构造函数的参数注入给依赖对象,并且在初始化对象的时候注入。
优点:
对象初始化完成后便可获得可使用的对象。
缺点:
当需要注入的对象很多时,构造器参数列表将会很长;
不够灵活。若有多种注入方式,每种方式只需注入指定几个依赖,那么就需要提供多个重载的构造函
数,麻烦。

二、setter 方法注入
IoC Service Provider 通过调用成员变量提供的 setter 函数将被依赖对象注入给依赖类。
优点:
灵活。可以选择性地注入需要的对象。
缺点:
依赖对象初始化完成后由于尚未注入被依赖对象,因此还不能使用。

三、接口注入
依赖类必须要实现指定的接口,然后实现该接口中的一个函数,该函数就是用于依赖注入。该函数的参
数就是要注入的对象。
优点
接口注入中,接口的名字、函数的名字都不重要,只要保证函数的参数是要注入的对象类型即可。
缺点:
侵入行太强,不建议使用。



 

 

posted @ 2017-10-13 11:43  卖雨伞的小男孩  阅读(513)  评论(0编辑  收藏  举报