精华内容
下载资源
问答
  • 依赖倒置 控制反转 依赖注入 面向
  • 不管怎样我们都是为了提倡高内聚和低耦合的思想,这么多种思想是不是看那些概念头晕的不行呢?这里我们主要列举吃饭的例子让大家更直观的理解这几个概念,现在有顾客(客户端)与餐厅(服务端)两个对象 依赖倒置...

     

    不管怎样我们都是为了提倡高内聚和低耦合的思想,这么多种思想是不是看那些概念头晕的不行呢?

    这里我们主要列举吃饭的例子让大家更直观的理解这几个概念,现在有顾客(客户端)与餐厅(服务端)两个对象

     

    依赖倒置: 餐厅建立订餐通道  (本来是顾客依赖餐厅炒菜的,开通饿了吗后餐厅就倒过来依赖ele的订单去炒菜了)


    控制反转IOC(Inversion Of Control):  改成自助餐厅(以前餐厅炒的菜分量太少了,现在菜都摆出来了你可以自己选择量多的菜了)


    依赖注入DI(Dependency Injection): 餐厅开通了代加工通道  (主要用于扩展  顾客可以自己带材料到餐厅炒了)


    面向切面AOP(Aspect Oriented Programming): 就是面向抽象的过程

     

    转载于:https://www.cnblogs.com/Wilson6/p/8797465.html

    展开全文
  • 依赖和耦合(Dependency and Coupling) 依赖依赖描述了两个模型元素之间的关系,如果被依赖的模型元素发生变化就会影响到另一个模型元素。 简单的说,依赖就是一种需要。鱼需要水才能生存,鱼对水就有依赖关系;...

    依赖和耦合(Dependency and Coupling)

    依赖:依赖描述了两个模型元素之间的关系,如果被依赖的模型元素发生变化就会影响到另一个模型元素。

    简单的说,依赖就是一种需要。鱼需要水才能生存,鱼对水就有依赖关系;人需要进食才能活着,人对食物就有依赖关系。

    耦合:如果改变程序的一个模块要求另一个模块同时发生变化,就认为这两个模块发生了耦合。

    简单地说,耦合就是发生了依赖。和上面的例子一样,鱼和水之间发生了耦合,如果水发生了改变,会影响到鱼;人和食物之间发生了耦合,如果食物发生了改变,也会对人造成影响。

    从上面的定义中可以看出,如果模块A调用模块B提供的方法,或者访问模块B中的某些数据成员(当然,在面向对象开发中一般不提倡这样做),我们就认为模块A依赖于模块B,模块A和模块B之间发生了耦合。

    class Go {
        Run r = new Run(); // 对Run产生了依赖关系,即Go和Run发生了耦合关系
        void doWalk() {
            r.doRun();
        }
    }
    
    class Run {
        void doRun() {
            // do something
        }
    }
    

    Ioc

    降低紧耦合场景,将控制权移交给一个“代理”进行统一处理;
    控制反转从字面上来看,就是对控制权的反转。把创建对象的控制权交给第三方容器,当程序中需要用到实例对象的时候,就向第三方容器发出请求,由第三方容器返回一个实例对象。

    在这里插入图片描述

    DI

    依赖注入(Dependency Injection,简称 DI)是实现控制反转的主要方式:在类 A 的实例创建过程中就创建了依赖的 B 对象,通过类型或名称来判断将不同的对象注入到不同的属性中;
    依赖注入是控制反转的具体实现。意思就是说当程序中需要用到实例对象的时候,才去向第三方容器发出请求,由第三方容器返回实例对象(注入)。

    注入三种实现方式

    方法1:构造函数注入:在构造的时候,将依赖的ClassB对象作为参数传递给ClassA对象。(ClassA中有reference等着被赋值)

    方法2:setter注入:通过ClassA对象的setter方法,将ClassB对象作为参数传递给ClassA对象。(ClassA中有reference等着被赋值)

    方法3:定义一个接口A,ClassA类实现接口A的某个方法,将ClassB对象作为参数传递给ClassA对象。(ClassA中有reference等着被赋值)

    方法2和方法3的区别还是解耦,任何类只要实现了接口A的这个方法都有依赖注入(依赖赋值)能力。

    假定 Driveable为上述提到的ClassB;
    Person2为ClassA;
    DepedencySetter为接口A

    /**
     * 接口方式注入
     * 接口的存在,表明了一种依赖配置的能力。
     */
    public interface DepedencySetter {
        void set(Driveable driveable);
    }
    public class Person2  implements DepedencySetter {
    
        //接口方式注入
        @Override
        public void set(Driveable driveable) {
            this.mDriveable = mDriveable;
        }
    
        private Driveable mDriveable;
    
        //构造函数注入
        public Person2(Driveable driveable){
            this.mDriveable = driveable;
        }
    
        //setter 方式注入
        public void setDriveable(Driveable mDriveable) {
            this.mDriveable = mDriveable;
        }
    
        public void goOut(){
            System.out.println("出门啦");
            mDriveable.drive();
            //mCar.drive();
    //        mTrain.drive();
        }
    
        public static void main(String ... args){
                //TODO:
            Person2 person = new Person2(new Car());
            person.goOut();
        }
    }
    

    AOP

    展开全文
  • 我们可以采用若干设计模式以不同的方式实现IoC,比如我们在《依赖注入[2]: 基于IoC的设计模式》介绍的模板方法、工厂方法和抽象工厂,接下来我们介绍一种更为有价值的IoC模式,即依赖注入(DI:Dependency Injection...

    IoC主要体现了这样一种设计思想:通过将一组通用流程的控制权从应用转移到框架中以实现对流程的复用,并按照“好莱坞法则”实现应用程序的代码与框架之间的交互。我们可以采用若干设计模式以不同的方式实现IoC,比如我们在《依赖注入[2]: 基于IoC的设计模式》介绍的模板方法、工厂方法和抽象工厂,接下来我们介绍一种更为有价值的IoC模式,即依赖注入(DI:Dependency Injection,以下简称DI)。

    目录
    一、由容器提供服务实例
    二、构造器注入
    三、属性注入
    四、方法注入
    五、Service Locator

    一、由容器提供服务实例

    和在《基于IoC的设计模式》中介绍的工厂方法和抽象工厂模式一样,DI是一种“对象提供型”的设计模式,在这里我们将提供的对象统称为“服务”、“服务对象”或者“服务实例”。在一个采用DI的应用中,在定义某个服务类型的时候,我们直接将依赖的服务采用相应的方式注入进来。按照“面向接口编程”的原则,被注入的最好是依赖服务的接口而非实现。

    在应用启动的时候,我们会对所需的服务进行全局注册。服务一般都是针对接口进行注册的,服务注册信息的核心目的是为了在后续消费过程中能够根据接口创建或者提供对应的服务实例。按照“好莱坞法则”,应用只需要定义好所需的服务,服务实例的激活和调用则完全交给框架来完成,而框架则会采用一个独立的“容器(Container)”来提供所需的每一个服务实例。

    我们将这个被框架用来提供服务的容器称为“DI容器”,也由很多人将其称为“IoC容器”,根据我们在《控制反转》针对IoC的介绍,我不认为后者是一个合理的称谓。DI容器之所以能够按照我们希望的方式来提供所需的服务是因为该容器是根据服务注册信息来创建的,服务注册了包含提供所需服务实例的所有信息。

    举个简单的例子,我们创建一个名为Cat的DI容器类,那么我们可以通过调用具有如下定义的扩展方法GetService<T>从某个Cat对象获取指定类型的服务对象。我之所以将其命名为Cat,源于我们大家都非常熟悉的一个卡通形象“机器猫(哆啦A梦)”。机器猫的那个四次元口袋就是一个理想的DI容器,大熊只需要告诉哆啦A梦相应的需求,它就能从这个口袋中得到相应的法宝。DI容器亦是如此,服务消费者只需要告诉容器所需服务的类型(一般是一个服务接口或者抽象服务类),就能得到与之匹配的服务实例。

    public static class CatExtensions
    {  
        public static T GetService<T>(this Cat cat);
    }
     

    对于演示的MVC框架,我们在《基于IoC的设计模式》中分别采用不同的设计模式对框架的核心类型MvcEngine进行了改造,现在我们采用DI的方式并利用上述的这个Cat容器按照如下的方式对其进行重新实现,我们会发现MvcEngine变得异常简洁而清晰。

    复制代码
    public class MvcEngine
    {
        public Cat Cat { get; }
        public MvcEngine(Cat cat) => Cat = cat;
            
        public async Task StartAsync(Uri address)
        {
            var listener = Cat.GetService<IWebLister>();
            var activator = Cat.GetService<IControllerActivator>();
            var executor = Cat.GetService<IControllerExecutor>();
            var render = Cat.GetService<IViewRender>();
            await listener.ListenAsync(address);
            while (true)
            {
                var httpContext = await listener.ReceiveAsync();
                var controller = await activator.CreateControllerAsync(httpContext);
                try
                {
                    var view = await executor.ExecuteAsync(controller, httpContext);
                    await render.RendAsync(view, httpContext);
                }
                finally
                {
                    await activator.ReleaseAsync(controller);
                }
            }
        }  
    }
    复制代码
     

    从服务消费的角度来讲,我们借助于一个服务接口对消费的服务进行抽象,那么服务消费程序针对具体服务类型的依赖可以转移到对服务接口的依赖上,但是在运行时提供给消费者总是一个针对某个具体服务类型的对象。不仅如此,要完成定义在服务接口的操作,这个对象可能需要其他相关对象的参与,也就是说提供的这个服务对象可能具有针对其他对象的依赖。作为服务对象提供者的DI容器,在它向消费者提供服务对象之前就会根据服务实现类型和服务注册信息自动创建依赖的服务实例,并将后者注入到当前对象之中。接下来我们从编程层面介绍三种典型的注入方式。

    二、构造器注入

    构造器注入就在在构造函数中借助参数将依赖的对象注入到创建的对象之中。如下面的代码片段所示,Foo针对Bar的依赖体现在只读属性Bar上,针对该属性的初始化实现在构造函数中,具体的属性值由构造函数的传入的参数提供。当DI容器通过调用构造函数创建一个Foo对象之前,需要根据当前注册的类型匹配关系以及其他相关的注入信息创建并初始化参数对象。

    public class Foo
    {
        public IBar Bar{get;}
        public Foo(IBar bar) =>Bar = bar;
    }

    除此之外,构造器注入还体现在对构造函数的选择上面。如下面的代码片段所示,Foo类上面定义了两个构造函数,DI容器在创建Foo对象之前首选需要选择一个适合的构造函数。至于目标构造函数如何选择,不同的DI容器可能有不同的策略,比如可以选择参数做多或者最少的,或者可以按照如下所示的方式在目标构造函数上标注一个InjectionAttribute特性。

    复制代码
    public class Foo
    {
        public IBar Bar{get;}
        public IBaz Baz {get;}
    
        [Injection]
        public Foo(IBar bar) =>Bar = bar;
        public Foo(IBar bar, IBaz):this(bar) =>Baz = baz;
    }
    复制代码
     

    三、属性注入

    如果依赖直接体现为类的某个属性,并且该属性不是只读的,我们可以让DI容器在对象创建之后自动对其进行赋值进而达到依赖自动注入的目的。一般来说,我们在定义这种类型的时候,需要显式将这样的属性标识为需要自动注入的依赖属性以区别于该类型的其他普通的属性。如下面的代码片段所示,Foo类中定义了两个可读写的公共属性Bar和Baz,我们通过标注InjectionAttribute特性的方式将属性Baz设置为自动注入的依赖属性。对于由DI容器提供的Foo对象,它的Baz属性将会自动被初始化。

    复制代码
    public class Foo
    {
        public IBar Bar{get; set;}
    
        [Injection]
        public IBaz Baz {get; set;}
    }
    复制代码

    四、方法注入

    体现依赖关系的字段或者属性可以通过方法的形式初始化。如下面的代码片段所示,Foo针对Bar的依赖体现在只读属性上,针对该属性的初始化实现在Initialize方法中,具体的属性值由构造函数的传入的参数提供。我们同样通过标注特性(InjectionAttribute)的方式将该方法标识为注入方法。DI容器在调用构造函数创建一个Foo对象之后,它会自动调用这个Initialize方法对只读属性Bar进行赋值。在调用该方法之前,DI容器会根据预先注册的类型映射和其他相关的注入信息初始化该方法的参数。

    复制代码
    public class Foo
    {
        public IBar Bar{get;}
    
        [Injection]
        public Initialize(IBar bar)=> Bar = bar;
    }
    复制代码
     

    除了上述这种通过DI容器在初始化服务过程中自动调用的实现在外,我们还可以利用它实现另一个更加自由的方法注入形式,后者在ASP.NET Core应用具有广泛的应用。ASP.NET Core在启动的时候会调用我们注册的Startup对象来完成中间件的注册,当我们在定义这个Startup类型的时候不需要让它实现某个接口,所以用于注册中间件的Configure方法其实没有一个固定的声明,我们可以按照如下的方式将任意依赖的服务直接注入到这个方法中。

    public class Startup
    {
        public void Configure(IApplicationBuilder app, IFoo foo, IBar bar, IBaz baz);
    }

    类似的注入方式同样可以应用到中间件的定义中。与用于注册中间件的Startup类型一样,ASP.NET Core框架下的中间件类型同样不需要实现某个预定义的接口,用于处理请求的InvokeAsync或者Invoke方法上同样可以按照如下的方式注入任意的依赖服务。

     

    public class FoobarMiddleware
    {
        private readonly RequestDelegate _next; 
        public FoobarMiddleware(RequestDelegate next) =>_next = next;
        public Task InvokeAsync(HttpContext httpContext, IFoo foo, IBar bar, IBaz baz);
    }

    上面这种方式的方法注入促成了一种“面向约定”的编程方式,由于不再需要实现某个预定义的接口或者继承某一个预定义的类型,需要实现的方法的声明也就少了对应的限制,这样就可用采用最直接的方式将依赖的服务注入到所需的方法中。

    对于上面介绍的这几种注入方式,构造器注入是最为理想的形式,我个人不建议使用属性注入和方法注入(上面介绍这种基于约定的方法注入除外)。我们定义的服务类型应该是独立自治的,我们不应该对它运行的环境做过多的假设和限制,也就说同一个服务类型可以使用在框架A中,也可以实现在框架B上;在没有使用任何DI容器的应用中可以使用这个服务类型,当任何一种DI容器被使用到应用中之后,该服务类型依旧能够被正常使用。对于上面介绍的这三种注入方式,唯一构造器注入能够代码这个目的,而属性注入和方法注入都依赖于某个具体的DI框架来实现针对依赖属性的自动复制和依赖方法的自动调用。

    五、Service Locator

    假设我们需要定义一个服务类型Foo,它依赖于另外两个服务Bar和Baz,后者对应的服务接口分别为IBar和IBaz。如果当前应用中具有一个DI容器(假设类似于我们在上面定义的Cat),那么我们可以采用如下两种方式来定义这个服务类型Foo。

    复制代码
    public class Foo : IFoo
    {
        public IBar Bar { get; }
        public IBaz Baz { get; }
        public Foo(IBar bar, IBaz baz)
        {
            Bar = bar;
            Baz = baz;
        }  
        public async Task InvokeAsync()
        {
            await Bar.InvokeAsync();
            await Baz.InvokeAsync();
        }
    }
    
    public class Foo : IFoo
    {
        public Cat Cat { get; }
        public Foo(Cat cat) => Cat = cat; 
        public async Task InvokeAsync()
        {
            await Cat.GetService<IBar>().InvokeAsync();
            await Cat.GetService<IBaz>().InvokeAsync();
        }
    }
    复制代码

    从表面上看,上面提供的这两种服务类型的定义方式貌似都不错,至少它们都解决针对依赖服务的耦合问题,将针对服务实现的依赖转变成针对接口的依赖。那么哪一种更好呢?我想有人会选择第二种定义方式,因为这种定义方式不仅仅代码量更少,针对服务的提供也更加直接。我们直接在构造函数中“注入”了代表“DI容器”的Cat对象,在任何使用到依赖服务的地方,我们只需要利用它来提供对应的服务实例就可以了。

    但事实上第二种定义方式采用的设计模式根本就不是“依赖注入”,而是一种被称为“Service Locator”的设计模式。Service Locator模式同样具有一个通过服务注册创建的全局的容器来提供所需的服务实例,该容器被称为“Service Locator”。“DI容器”和“Service Locator”实际上是同一事物在不同设计模型中的不同称谓罢了,那么DI和Service Locator之间的差异体现在什么地方呢?

    我们觉得可以从“DI容器”和“Service Locator”被谁使用的角度来区分这两种设计模式的差别。在一个采用依赖注入的应用中,我们只需要采用标准的注入形式将服务类型定义好,并在应用启动之前完成相应的服务注册就可以了,框架自身的引擎在运行过程中会利用DI容器来提供当前所需的服务实例。换句话说,DI容器的使用者应该是框架而不是应用程序。Service Locator模式显然不是这样,很明显是应用程序在利用它来提供所需的服务实例,所以它的使用者是应用程序

    我们也可以从另外一个角度区分两者之间的差别。由于依赖服务是以“注入”的方式来提供的,所以采用依赖注入模式的应用可以看成是将服务“”给DI容器,Service Locator模式下的应用则是利用Service Locator去“拉”取所需的服务,这一推一拉也准确地体现了两者之间的差异。那么既然两者之间有差别,究竟孰优孰劣呢?

    早在2010年,Mark Seemann就在它的博客中将Service Locator视为一种“反模式(Anti-Pattern)”,虽然也有人对此提出不同的意见,但我个人是非常不推荐使用这种设计模式的。我反对使用Service Locator与上面提到的反对使用属性注入和方法注入具有类似的缘由。

    我们既然将一组相关的操作定义在一个能够复用的服务中,不但要求服务自身具有独立和自治的特性,也要求服务之间的应该具有明确的边界,服务之间的依赖关系应该是明确的而不是模糊的。不论是采用属性注入或者构造器注入,还是使用Service Locator来提供当前依赖的服务,这无疑为当前的应用增添了一个新的依赖,即针对DI容器或者Service Locator的依赖。

    当前服务针对另一个服务的依赖与针对DI容器或者Service Locator的依赖具有本质的不同,前者是一种基于类型的依赖,不论是基于服务的接口还是实现类型,这是一种基于“契约”的依赖。这种依赖不仅是明确的,也是由保障的。但是DI容器也好,Service Locator也罢,它们本质上都是一个黑盒,它能够提供所需服务的前提已经预先添加了对应的服务注册,但是这种依赖不仅是模糊和也是可靠的。

    正因为如此,ASP.NET Core框架使用的DI框架只支持构造器注入,而不支持属性和方法注入(类似于Startup和中间件基于约定的方法注入除外)。但是我们很有可能不知不觉地会按照Service Locator模式来编写我们的代码,从某种意义上讲,当我们在程序中使用IServiceProvider(表示DI容器)来提取某个服务实例的时候,就意味着我们已经在使用Service Locator模式了,所以当我们遇到这种情况下的时候应该多想一想是否一定需要这么做。虽然我们提倡尽可能避免使用Service Locator模式,但是有的时候(有其是在编写框架或者组件的时候),我们是无法避免使用IServiceProvider来提取服务。

    依赖注入[1]: 控制反转
    依赖注入[2]: 基于IoC的设计模式
    依赖注入[3]: 依赖注入模式
    依赖注入[4]: 创建一个简易版的DI框架[上篇]
    依赖注入[5]: 创建一个简易版的DI框架[下篇]
    依赖注入[6]: .NET Core DI框架[编程体验]
    依赖注入[7]: .NET Core DI框架[服务注册]
    依赖注入[8]: .NET Core DI框架[服务消费]

    作者:蒋金楠
    微信公众账号:大内老A
    微博: www.weibo.com/artech
    如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号)。
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
     

    转载于:https://www.cnblogs.com/linybo/p/10053137.html

    展开全文
  • 依赖倒置 控制反转 依赖注入
      
    

    转自http://dotnetfresh.cnblogs.com/archive/2005/06/27/181878.html




    依赖倒置、控制反转和依赖注入辨析

     

    在《道法自然——面向对象实践指南》一书中,我们采用了一个对立统一的辩证关系来说明“模板方法”模式—— “正向依赖 vs. 依赖倒置”(参见:《道法自然》第15章[王咏武, 王咏刚 2004])。这种把“好莱坞”原则和 “依赖倒置”原则等量齐观的看法其实来自于轻量级容器PicoContainer主页上的一段话:

    控制反转(Inversion of Control)的一个著名的同义原则是由Robert C. Martin提出的依赖倒置原则(Dependency Inversion Principle),它的另一个昵称是好莱坞原则(Hollywood Principle:不要调用我,让我来调用你)”[PicoContainer 2004]。

    和网友们在CSDN Blog上进行了深入的讨论后,我又把这些概念重新梳理了一下。我发现,这几个概念虽然在思路和动机等宏观层面上是统一的,但在具体的应用层面还是存在着许多很微妙的差别。本文通过几个简单的例子对依赖倒置(Dependency Inversion Principle)、控制反转(Inversion of Control)、依赖注入(Dependency Injection)等概念进行了更为深入的辨析,也算是对于《道法自然》正文内容的一个补充吧。

    1.      依赖和耦合(Dependency and Coupling) 

    在《道法自然——面向对象实践指南》一书中,我们采用了一个对立统一的辩证关系来说明“模板方法”模式—— “正向依赖 vs. 依赖倒置”(参见:《道法自然》第15章[王咏武, 王咏刚 2004])。这种把“好莱坞”原则和 “依赖倒置”原则等量齐观的看法其实来自于轻量级容器PicoContainer主页上的一段话:

    首先来看一下依赖和耦合的概念。

    Rational Rose的帮助文档上是这样定义“依赖”关系的:“依赖描述了两个模型元素之间的关系,如果被依赖的模型元素发生变化就会影响到另一个模型元素。典型的,在类图上,依赖关系表明客户类的操作会调用服务器类的操作。”

    Martin Fowler在《Reducing Coupling》一文中这样描述耦合:“如果改变程序的一个模块要求另一个模块同时发生变化,就认为这两个模块发生了耦合。” [Fowler 2001]

    从上面的定义可以看出:如果模块A调用模块B提供的方法,或访问模块B中的某些数据成员(当然,在面向对象开发中一般不提倡这样做),我们就认为模块A依赖于模块B,模块A和模块B之间发生了耦合。
      那么,依赖对于我们来说究竟是好事还是坏事呢?
      由于人类的理解力有限,大多数人难以理解和把握过于复杂的系统。把软件系统划分成多个模块,可以有效控制模块的复杂度,使每个模块都易于理解和维护。但在这种情况下,模块之间就必须以某种方式交换信息,也就是必然要发生某种耦合关系。如果某个模块和其它模块没有任何关联(哪怕只是潜在的或隐含的依赖关系),我们就几乎可以断定,该模块不属于此软件系统,应该从系统中剔除。如果所有模块之间都没有任何耦合关系,其结果必然是:整个软件不过是多个互不相干的系统的简单堆积,对每个系统而言,所有功能还是要在一个模块中实现,这等于没有做任何模块的分解。

    因此,模块之间必定会有这样或那样的依赖关系,永远不要幻想消除所有依赖。但是,过强的耦合关系(如一个模块的变化会造成一个或多个其他模块也同时发生变化的依赖关系)会对软件系统的质量造成很大的危害。特别是当需求发生变化时,代码的维护成本将非常高。所以,我们必须想尽办法来控制和消解不必要的耦合,特别是那种会导致其它模块发生不可控变化的依赖关系。依赖倒置、控制反转、依赖注入等原则就是人们在和依赖关系进行艰苦卓绝的斗争过程中不断产生和发展起来的。

    2.      接口和实现分离 

    把接口和实现分开是人们试图控制依赖关系的第一个尝试,图 1是Robert C. Martin在《依赖倒置》[Martin 1996]一文中所举的第一个例子。其中,ReadKeyboard()和WritePrinter()为函数库中的两个函数,应用程序循环调用这两个函数,以便把用户键入的字符拷贝到打印机输出。

     

    为了使应用程序不依赖于函数库的具体实现,C语言把函数的定义写在了一个分离的头文件(函数库.h)中。这种做法的好处是:虽然应用程序要调用函数库、依赖于函数库,但是,当我们要改变函数库的实现时,只要重写函数的实现代码,应用程序无需发生变化。例如,改变函数库.c文件,把WritePrinter()函数重新实现成向磁盘中输出,这时只要将应用程序和函数库重新链接,程序的功能就会发生相应的变化。

    上面的函数库也可以采用C++语言来实现。我们通常把这种用面向对象技术实现的,为应用程序提供多个支持类的模块称为 “类库”,如图 2所示。这种通过分离接口和实现来消解应用程序和类库之间依赖关系的做法具有以下

    特点

    1. 应用程序调用类库,依赖于类库。

    2. 接口和实现的分离从一定的程度上消解了这个依赖关系,具体实现可以在编译期间发生变化。但是,这种消解方法的作用非常有限。比如说,一个系统中无法容纳多个实现,不同的实现不能动态发生变化,用WritePrinter函数名来实现向磁盘中输出的功能也显得非常古怪,等等。

    3. 类库可以单独重用。但是应用程序不能脱离类库而重用,除非提供一个实现了相同接口的类库。

     

    3.      依赖倒置(Dependency Inversion Principle) 

    可以看出,上面讨论的简单分离接口的方法对于依赖关系的消解作用非常有限。Java语言提供了纯粹的接口类,这种接口类不包括任何实现代码,可以更好地隔离两个模块。C++语言中虽然没有定义这种纯粹的接口类,但所有成员函数都是纯虚函数的抽象类也不包含任何实现代码,可以起到类似于Java接口类的作用。为了和上一节中提到的简单接口相区别,本文后面将把基于Java 接口类或C++抽象类定义的接口称为抽象接口。

    依赖倒置原则就是建立在抽象接口的基础上的。从依赖具象倒置为依赖抽象。

    Robert Martin这样描述依赖倒置原则[Martin 1996]:

    A. 上层模块不应该依赖于下层模块,它们共同依赖于一个抽象。

    B. 抽象不能依赖于具象,具象依赖于抽象。

    其含义是:为了消解两个模块间的依赖关系,应该在两个模块之间定义一个抽象接口,上层模块调用抽象接口定义的函数,下层模块实现该接口。如图 3所示,对于上一节的例子,我们可以定义两个抽象类Reader和Writer作为抽象接口,其中的Read()和Write()函数都是纯虚函数,而具体的KeyboardReader和PrinterWriter类实现了这些接口。当应用程序调用Read()和Write()函数时,由于多态性机制的作用,实际调用的是具体的KeyboardReader和PrinterWriter类中的实现。因此,抽象接口隔离了应用程序和类库中的具体类,使它们之间没有直接的耦合关系,可以独立地扩展或重用。例如,我们可以用类似的方法实现FileReader或DiskWriter类,应用程序既可以根据需要选择从键盘或文件输入,也可以选择向打印机或磁盘输出,甚至同时完成多种不同的输入、输出任务。由此可以总结出,这种通过抽象接口消解应用程序和类库之间依赖关系的做法具有以下

    特点

    1. 应用程序调用类库的抽象接口,依赖于类库的抽象接口;具体的实现类派生自类库的抽象接口,也依赖于类库的抽象接口。

    2. 应用程序和具体的类库实现完全独立,相互之间没有直接的依赖关系,只要保持接口类的稳定,应用程序和类库的具体实现都可以独立地发生变化。

    3. 类库完全可以独立重用,应用程序可以和任何一个实现了相同抽象接口的类库协同工作。

     

     

    注:这里对具象方法的调用是在“应用程序类”中完成的。作为对比,后面介绍“控制反转”时,会通过模板方法将这个调用过程封装起来。通过对比,明确二者的区别于联系。

     

    一般情况下,由于类库的设计者并不知道应用程序会如何使用类库,抽象接口大多由类库设计者根据自己设想的典型使用模式总结出来,并保留一定的灵活度,以提供给应用程序的开发者使用。

    (上面的例子是一个部分的依赖倒置,下面是一个完全的依赖倒置)

    但还有另外一种情况。图 4是Martin Fowler在《Reducing Coupling》一文中使用的一个例子[Fowler 2001]。其中,Domain包要使用数据库包,即Domain包依赖于数据库包。为了隔离Domain包和数据库包,可以引入一个Mapper包。如果在特定的情况下,我们希望Domain包能够被多次重用,而Mapper包可以随时变化,那么,我们就必须防止Domain包过分地依赖于Mapper包。这时,可以由 Domain包的设计者总结出自己需要的抽象接口(如Store),而由Mapper包的设计者来实现该抽象接口。这样一来,无论是在接口层面,还是在实现层面,依赖关系都完全颠倒过来了。  

     

     

    由调用者(Domain)对被调用者(Mapper)的依赖,变成了被调用者(Mapper)对调用者(Domain)的依赖。

    4.      控制反转(Inversion of Control) 

    前面描述的是应用程序类库之间的依赖关系。如果我们开发的不是类库,而是框架系统,依赖关系就会更强烈一点。那么,该如何消解框架和应用程序之间的依赖关系呢?

    《道法自然》第5章描述了框架和类库之间的区别:

    框架和类库最重要的区别是:框架是一个半成品的应用程序,而类库只包含一系列可被应用程序调用的类。

    “类库给用户提供了一系列可复用的类,这些类的设计都符合面向对象原则和模式。用户使用时,可以创建这些类的实例,或从这些类中继承出新的派生类,然后调用类中相应的功能。在这一过程中,类库总是被动地响应用户的调用请求。

    “框架则会为某一特定目的实现一个基本的、可执行的架构。框架中已经包含了应用程序从启动到运行的主要流程,流程中那些无法预先确定的步骤留给用户来实现。程序运行时,框架系统自动调用用户实现的功能组件。这时,框架系统的行为是主动的。

    “我们可以说,类库是死的,而框架是活的。应用程序通过调用类库来完成特定的功能,而框架则通过调用应用程序来实现整个操作流程。框架是控制倒置原则的完美体现。”

    框架系统的一个最好的例子就是图形用户界面(GUI)系统。一个简单的,使用面向过程的设计方法开发的GUI系统如图 5所示。


     从图 5中可以看出,应用程序调用GUI框架中的CreateWindow()函数来创建窗口,在这里,我们可以说应用程序依赖于GUI框架。但GUI框架并不了解该窗口接收到窗口消息后应该如何处理,这一点只有应用程序最为清楚。因此,当GUI框架需要发送窗口消息时,又必须调用应用程序定义的某个特定的窗口函数(如上图中的MyWindowProc)。这时,GUI框架又必须依赖于应用程序。这是一个典型的双向依赖关系。这种双向依赖关系有一个非常严重的缺陷:由于GUI框架调用了应用程序中的某个特定函数(MyWindowProc), GUI框架根本无法独立存在;换一个新的应用程序,GUI框架多半就要做相应的修改。因此,如何消解框架系统对应用程序的依赖关系是实现框架系统的关键。

    并非只有面向对象的方法才能解决这一问题。WIN32 API早就为我们提供了在面向过程的设计思路下解决类似问题的范例。类WIN32 的架构模型如图 6所示。

     

    在图 6中,应用程序调用CreateWindow()函数时,要传递一个消息处理函数的指针给GUI框架(对WIN32而言,我们在注册窗口类时传递这一指针),GUI框架把该指针记录在窗口信息结构中。需要发送窗口消息时,GUI框架就通过该指针调用窗口函数。和图 5 相比,GUI框架仍然需要调用应用程序,但这一调用从一个硬编码的函数调用变成了一个由应用程序事先注册被调用对象的动态调用。图 6用一条虚线表示这种动态调用。可以看出,这种动态的调用关系有一个非常大的好处:当应用程序发生变化时,它可以自行改变框架系统的调用目标,GUI框架无需随之发生变化。现在,我们可以说,虽然还存在着从GUI框架到应用程序的调用关系,但GUI框架已经完全不再依赖于应用程序了。这种动态调用机制通常也被称为“回调函数”。

    在面向对象领域,“回调函数”的替代物就是“模板方法模式”,也就是“好莱坞原则(不要调用我们,让我们调用你)”。GUI框架的一个面向对象的实现如图 7所示。

     

    图 7中,“GUI框架抽象接口”是GUI框架系统提供给应用程序使用的接口。

    抽象出该接口的动机是根据“依赖倒置”的原则,消解从应用程序到GUI框架之间的直接依赖关系,以使得GUI框架实现的变化对应用程序的影响最小化。

    Window接口类则是“模板方法模式”的核心。应用程序调用CreateWindow()函数时,GUI框架会把该窗口的引用保存在窗口链表中。需要发送窗口消息时,GUI框架就调用窗口对象的SendMessage()函数,该函数是实现在Window类中的非虚成员函数。SendMessage()函数又调用WindowProc()虚函数,这里实际执行的是应用程序MyWindow类中实现的WindowProc()函数。在图 7中,我们已经看不到从GUI框架到应用程序之间的直接依赖关系了。因此,模板方法模式完全实现了回调函数的动态调用机制,消解了从框架到应用程序之间的依赖关系。

    从上面的分析可以看出,模板方法模式是框架系统的基础,任何框架系统都离不开模板方法模式。

    Martin Fowler也说 [Folwer 2004],“几位轻量级容器的作者曾骄傲地对我说:这些容器非常有用,因为它们实现了‘控制反转’。这样的说辞让我深感迷惑:控制反转是框架所共有的特征,如果仅仅因为使用了控制反转就认为这些轻量级容器与众不同,就好像在说‘我的轿车是与众不同的,因为它有四个轮子’。问题的关键在于:它们反转了哪方面的控制?我第一次接触到的控制反转针对的是用户界面的主控权。早期的用户界面是完全由应用程序来控制的,你预先设计一系列命令,例如‘输入姓名’、‘输入地址’等,应用程序逐条输出提示信息,并取回用户的响应。而在图形用户界面环境下,UI 框架将负责执行一个主循环,你的应用程序只需为屏幕的各个区域提供事件处理函数即可。在这里,程序的主控权发生了反转:从应用程序移到了框架。”

    确实:对比图 3和图 7可以看出,使用普通类库时,程序的主循环位于应用程序中,而使用框架系统的应用程序不再包括一个主循环,只是实现某些框架定义的接口,框架系统负责实现系统运行的主循环,并在必要的时候通过模板方法模式调用应用程序。

    也就是说,虽然“依赖倒置”和“控制反转”在设计层面上都是消解模块耦合的有效方法,也都是试图令具体的、易变的模块依赖于抽象的、稳定的模块的基本原则,但二者在使用语境和关注点上存在差异

    1.       “依赖倒置”强调的是对于传统的、源于面向过程设计思想的层次概念的“倒置”,而“控制反转”强调的是对程序流程控制权的反转;

    2.       “依赖倒置”的使用范围更为宽泛,既可用于对程序流程的描述(如流程的主从和层次关系),也可用于描述其他拥有概念层次的设计模型(如服务组件与客户组件、核心模块与外围应用等),而

    “控制反转”则仅适用于描述流程控制权的场合(如算法流程或业务流程的控制权)。

    从某种意义上说,我们也可以把“控制反转”看作是“依赖倒置”的一个特例。例如,用模板方法模式实现的“控制反转”机制其实就是在框架系统和应用程序之间抽象出了一个描述所有算法步骤原型的接口类,框架系统依赖于该接口类定义并实现程序流程,应用程序依赖于该接口类提供具体算法步骤的实现,应用程序对框架系统的依赖被“倒置”为二者对抽象接口的依赖。
      总地说来,应用程序和框架系统之间的依赖关系有以下特点:

    1. 应用程序和框架系统之间实际上是双向调用,双向依赖的关系。

    2. 依赖倒置原则可以减弱应用程序到框架之间的依赖关系。

    3. “控制反转”及具体的模板方法模式可以消解框架到应用程序之间的依赖关系,这也是所有框架系统的基础。

    4. 框架系统可以独立重用。

    5.      依赖注入(Dependency Injection) 

    在前面的例子里,我们通过“依赖倒置”原则,最大限度地减弱了应用程序Copy类和类库提供的服务Read,Write之间的依赖关系。但是,如果需要把Copy()函数也实现在类库中,又会发生什么情况呢?假设在类库中实现一个“服务类”,“服务类”提供Copy()方法供应用程序使用。应用程序使用时,首先创建“服务类”的实例,调用其中的Copy()函数。“服务类”的实例初始化时会创建KeyboardReader 和PrinterWriter类的实例对象。如图 8所示。

     

    从图 8中可以看出,虽然Reader和Writer接口隔离了“服务类”和具体的Reader和Writer类,使它们之间的耦合降到了最小。但当 “服务类”创建具体的Reader和Writer对象时,“服务类”还是和具体的Reader和Writer对象发生了依赖关系——图 8中用蓝色的虚线描述了这种依赖关系。

    在这种情况下,如何实例化具体的Reader和Writer类,同时又尽量减少服务类对它们的依赖,就是一个非常关键的问题了。如果服务类位于应用程序中,这一依赖关系对我们造成的影响还不算大。但当“服务类”位于需要独立发布的类库中,它的代码就不能随着应用程序的变化而改变了。这也意味着,如果“服务类”过度依赖于具体的Reader和Writer类,用户就无法自行添加新的Reader和Writer 的实现了。

    解决这一问题的方法是“依赖注入”,即切断“服务类”到具体的Reader和Writer类之间的依赖关系,而由应用程序来注入这一依赖关系。如图 9所示。

     

    在图 9中,“服务类”并不负责创建具体的Reader和Writer类的实例对象,而是由应用程序来创建。应用程序创建“服务类”的实例对象时,把具体的Reader和Write对象的引用注入“服务类”内部。这样,“服务类”中的代码就只和抽象接口相关的了。具体实现代码发生变化时,“服务类”不会发生任何变化。添加新的实现时,也只需要改变应用程序的代码,就可以定义并使用新的Reader和Writer类,这种依赖注入方式通常也被称为“构造器注入”。

    如果专门为Copy类抽象出一个注入接口,应用程序通过接口注入依赖关系,这种注入方式通常被称为“接口注入”。

    如果为Copy类提供一个设值函数,应用程序通过调用设值函数来注入依赖关系,这种依赖注入的方法被称为“设值注入”。具体的“接口注入”和“设值注入”请参考[Martin 2004]。
      PicoContainer和Spring轻量级容器框架都提供了相应的机制来帮助用户实现各种不同的“依赖注入”。并且,通过不同的方式,他们也都支持在XML文件中定义依赖关系,然后由应用程序调用框架来注入依赖关系,当依赖关系需要发生变化时,只要修改相应的 XML文件即可。

    因此,依赖注入的核心思想是:

    1. 抽象接口隔离了使用者和实现之间的依赖关系,但创建具体实现类的实例对象仍会造成对于具体实现的依赖

    2. 采用依赖注入可以消除这种创建依赖性。使用依赖注入后,某些类完全是基于抽象接口编写而成的,这可以最大限度地适应需求的变化。

    6.      结论 

      分离接口和实现是人们有效地控制依赖关系的最初尝试,而纯粹的抽象接口更好地隔离了相互依赖的两个模块,“依赖倒置”和 “控制反转”原则从不同的角度描述了利用抽象接口消解耦合的动机,GoF的设计模式正是这一动机的完美体现。具体类的创建过程是另一种常见的依赖关系,“依赖注入”模式可以把具体类的创建过程集中到合适的位置,这一动机和GoF的创建型模式有相似之处。
      这些原则对我们的实践有很好的指导作用,但它们不是圣经,在不同的场合可能会有不同的变化,我们应该在开发过程中根据需求变化的可能性灵活运用

    展开全文
  • 依赖倒置、控制反转和依赖注入辨析   在《道法自然——面向对象实践指南》一书中,我们采用了一个对立统一的辩证关系来说明“模板方法”模式—— “正向依赖 vs. 依赖倒置”(参见:《道法自然》第15章[王...
  • 控制反转和依赖注入

    2014-12-02 17:09:25
    依赖注入(DI/Dependence injection) : 容器创建好实例后再注入调用者称为依赖注入。   当某个角色(可能是一个Java实例,调用者)需要另一个角色(另一个Java实例,被调用者)的协助时,在传统的程序设计过程中,
  • 总结的来说,依赖注入是一种设计模式,因为它解决的是一类问题,这类问题是与依赖相关的。 依赖倒转原则 要知道依赖注入是解决什么问题,我们需要先了解一个原则:依赖倒转原则。 这是设计模式的六大原则之一,...
  •  IoC主要体现了这样一种设计思想:通过将一组通用流程的控制权从应用转移到框架中以...我们可以采用若干设计模式以不同的方式实现IoC,比如我们在《依赖注入[2]: 基于IoC的设计模式》介绍的模板方法、工厂方法和抽...
  • 在《道法自然——面向对象实践指南》一书中,我们采用了一个对立统一的辩证关系来说明“模板方法”模式—— “正向依赖 vs. 依赖倒置”(参见:《道法自然》第15章[王咏武, 王咏刚 2004])。这种把“好莱坞”原则和...
  • IoC 依赖注入容器 Unity

    2019-04-08 23:55:00
    IoC 依赖注入容器 Unity 原文:IoC 依赖注入容器 UnityIoC 是什么? 在软件工程领域,“控制反转(Inversion of Control,缩写为IoC)”是一种编程技术,表述在面向对象编程中,可描述为在编译时静态...
  • 在关于依赖注入,描述正确的是(BC)

    千次阅读 2020-08-04 22:15:24
    C、依赖注入能够降低系统各组件的依赖关系,提倡面向接口编程 D、Spring的依赖注入和控制反转是完全不同的两个概念 2、(多) 对于Spring的AOP的实现方式下列选项说法错误的是(AD) d A、Spring的Advice采用特定的aop...
  • 简介:Spring开发提倡面向接口编程,配合DI技术实现层与层的解耦。 1.依赖注入(DI):在应用运行期,由外部容器(spring),动态的将依赖对象注入到组件中。 2.激发POJO的潜能 POJO(Plain Old Java object):简单普通...
  • 依赖倒置、控制反转和依赖注入辨析 在《道法自然——面向对象实践指南》一书中,我们采用了一个对立统一的辩证关系来说明“模板方法”模式—— “正向依赖 vs. 依赖倒置”(参见:《道法自然》第15章[王咏武, ...
  • 控制反转与依赖注入

    2016-01-06 15:12:50
    控制反转与依赖注入控制反转(IoC/Inverse Of Control): 调用者不再创建被调用者的实例,由spring框架实现(容器创建)所以称为控制反转。依赖注入(DI/Dependence injection) : 容器创建好实例后再注入调用者称为...
  • IOC与依赖注入

    2013-08-14 14:14:27
     依赖倒置、控制反转和依赖注入辨析 在《道法自然——面向对象实践指南》一书中,我们采用了一个对立统一的辩证关系来说明“模板方法”模式—— “正向依赖 vs. 依赖倒置”(参见:《道法自然》第15章[王咏武, ...
  • 控制反转:框架控制应用/组件 依赖倒置: ...依赖注入:框架动态注入依赖关系到应用/组件 ---------------------------------------------------------------------------------- 在《道法自然——面向
  • 控制反转(Ioc)和依赖注入(DI) 控制反转IOC, 全称 “Inversion of Control”。依赖注入DI, 全称 “Dependency Injection”。 面向的问题:软件开发中,为了降低模块间、类间的耦合度,提倡基于接口的开发,那么在...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,180
精华内容 1,272
关键字:

依赖注入提倡面向