理解Spring的依赖倒置(DIP)、控制反转(IOC)和依赖注入(DI)

对象之间的关系:

依赖关系:依赖(Dependency)关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。

在这里插入图片描述

关联关系:在代码中通常将一个类的对象作为另一个类的成员变量来实现关联关系。老师与学生的关系中,每个老师可以教多个学生,每个学生也可向多个老师学,他们是双向关联。

在这里插入图片描述

聚合关系:聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。

在这里插入图片描述

组合关系:组合(Composition)关系也是关联关系的一种,也表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系,是 cxmtains-a 关系。
在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。例如,头和嘴的关系,没有了头,嘴也就不存在了

在这里插入图片描述

依赖倒置 (Dependency inversion principle)

依赖倒置是面向对象设计领域的一种软件设计原则。
Gof中的6大设计原则之一(合称 SOLID)。

阐述了模块之间的需要遵守的关联关系,依赖倒置原则的定义如下:

  • 上层模块不应该依赖底层模块,它们都应该依赖于抽象(模块之间需要通过一个介质(抽象,接口)来实现关联)。
  • 抽象不应该依赖于细节,具体细节依赖抽象。

依赖倒置简单理解就是:面向接口和抽象的编程。
通过面向接口编程使程序中模块的依赖程度降低,达到低耦合的功能,重用性加强。

控制反转(Inversion of control)

控制反转是一种OOD的思想,用来降低模块之间的耦合度。
在设计过程中,将设计好的对象,解析到LoC容器来进行控制,当程序执行过程中需要用到其他资源时(对象,文件,常量…)时,通过依赖注入(Dependency injection)将需要的资源对象通过控制反转到所需要的对象中去。

在不使用反转控制的程序中,“传统方式”是当我们需要用到其他对象的资源时,就直接通过“new”关键字创建一个对象。这这个过程中就容易造成对象之间关系之间依赖程度较高,导致类之间的耦合度高,不利于组件及代码的重用。

对象模块之间的关系类似于下图:


在这里插入图片描述

对象之间的依赖关系太强,一旦需要更改一个对象所在的类,其他的类对象都需要进行更改,可拓展性不高。
在下面这个例子中没有使用到loC,当客户类需要进行操作时,首先需要创建一个用户类,然后分析用户类需要一个依赖的用户信息类,然后创建用户信息类,在用户类中再创建用户信息类对象。操作过程中都是对象主动去创建需要的依赖对象。


在这里插入图片描述

使用loC后的对象之间的关系,是一种松耦合的关系,彼此依赖关系比较小,有利于类之间的独立工作。

在这里插入图片描述

在使用loC的过程中,对象之间的工作关系如下图:


在这里插入图片描述

各个对象之间通过loC容器进行关联,当程序运行到需要依赖对象时,由IoC容器将所需要的依赖对象通过依赖注入注入到对象中。

在下面的例子中,当用客户端类需要运行时,直接通过向IoC获取用户类,用户类所需要的用户信息类,容器已经直接给了用户类,不需要用户类再直接去找。(loC容器类似一个中介,当对象需要一个依赖对象时,不需要自己去创建,去寻找。只需要将自己所要的对象给loC容器描述一下,loC容器就可以直接帮你找到,并注入到对象中)。


在这里插入图片描述

在控制反转中通过loC容器通过反转来创建对象,并将依赖对象注入到对象中。反转的前提是使用XML文件将需要注入的对象向配置在文件中,在程序中通过
ClassPathXmlApplicationContext类来创建一个容器,通过容器的.getBean()方法来创建对象。通过反射,和XML中配置对象的Id或name属性来创建对象。

IoC 不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是 松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

其实IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。

IoC很好的体现了面向对象设计法则之一—— 好莱坞法则:“别找我们,我们找你”;即由IoC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。

举一个IOC例子

我们到餐厅去叫外卖,餐厅有专门送外卖的外卖员,他们的使命就是及时送达外卖食品。

依照依赖倒置原则,我们可以创建这样一个类。

public abstract class WaimaiYuan {
 
    protected Food food;
 
 
    public WaimaiYuan(Food food) {
        this.food = food;
    }
 
    abstract void songWaiMai();
 
}
 
public class Xiaohuozi extends WaimaiYuan {
 
 
    public Xiaohuozi(Food food) {
        super(food);
    }
 
    @Override
    void songWaiMai() {
        System.out.println("我是小伙子,为您送的外卖是:"+food);
 
    }
 
}
 
public class XiaoGuniang extends WaimaiYuan {
 
 
    public XiaoGuniang(Food food) {
        super(food);
    }
 
    @Override
    void songWaiMai() {
        System.out.println("我是小姑娘,为您送的外卖是:"+food);
    }
 
}

WaimaiYuan 是抽象类,代表送外卖的,Xiaohuozi 和 XiaoGuniang 是它的继承者,说明他们都可以送外卖。WaimaiYuan 都依赖于 Food,但是它没有实例化 Food 的权力。

再编写食物类代码

public abstract class Food {
    protected String name;
 
    @Override
    public String toString() {
        return name;
    }
 
}
 
public class PijiuYa extends Food {
 
    public PijiuYa() {
        name = "啤酒鸭";
    }
 
}
 
public class DuojiaoYutou extends Food {
 
    public DuojiaoYutou() {
        name = "剁椒鱼头";
    }
 
}

Food 是抽象类,PijiuYa 和 DuojiaoYutou 都是实现细节。

IoC 少不了 IoC 容器,也就是实例化抽象的地方。我们编写一个餐厅类。

public class Restaurant {
 
    public static void peican(int orderid,int flowid) {
        WaimaiYuan person;
        Food food;
 
        if ( orderid == 0) {
            food = new PijiuYa();
        } else {
            food = new DuojiaoYutou();
        }
 
        if ( flowid % 2 == 0 ) {
            person = new Xiaohuozi(food);
        } else {
            person = new XiaoGuniang(food);
        }
 
        person.songWaiMai();
 
    }
 
}

orderid 代表菜品编号,0 是啤酒鸭,其它则是剁椒鱼头。
flowid 是订单的流水号码。 餐厅根据流水编码的不同来指派小伙子或者小姑娘来送外卖,编写测试代码。

public class IocTest {
 
    public static void main(String[] args) {
 
        Restaurant.peican(0, 0);
        Restaurant.peican(0, 1);
        Restaurant.peican(1, 2);
        Restaurant.peican(0, 3);
        Restaurant.peican(1, 4);
        Restaurant.peican(1, 5);
        Restaurant.peican(1, 6);
        Restaurant.peican(0, 7);
        Restaurant.peican(0, 8);
    }
 
}

餐厅一次性送了 9 份外卖。

我是小伙子,为您送的外卖是:啤酒鸭
我是小姑娘,为您送的外卖是:啤酒鸭
我是小伙子,为您送的外卖是:剁椒鱼头
我是小姑娘,为您送的外卖是:啤酒鸭
我是小伙子,为您送的外卖是:剁椒鱼头
我是小姑娘,为您送的外卖是:剁椒鱼头
我是小伙子,为您送的外卖是:剁椒鱼头
我是小姑娘,为您送的外卖是:啤酒鸭
我是小伙子,为您送的外卖是:啤酒鸭

可以看到的是,因为有 Restaurant 这个 IoC 容器存在,大大地解放了外卖员的生产力,外卖员不再依赖具体的食物,具体的食物也不再依赖于特定的外卖员。也就是说,只要是食物外卖员就可以送,任何一种食物可以被任何一位外卖员送。

大家细细体会这是怎么样一种灵活性。如果非要外卖员自己决定配送什么食物,人少则还行,人多的时候,订单多的时候肯定会乱成一锅粥。

所以,实际工作当中,基本上都是按照专业的人干专业的事这种基本规律运行。外卖员没有能力也没有义务去亲自决定该送什么订单,这种权力在于餐厅,只要餐厅配置好就 OK 了。

记住配置这个词。

但作为 IoC 容器,无非是针对配置然后动态生成依赖关系。有的配置是开发者按照规则编写在 xml 格式文件中,有些配置则是利用 Java 中的反射与注解。

IoC 模式最核心的地方就是在于依赖方与被依赖方之间,也就是上文中说的上层模块与底层模块之间引入了第三方,这个第三方统称为 IoC 容器,因为 IoC 容器的介入,导致上层模块对于它的依赖的实例化控制权发生变化,也就是所谓的控制反转的意思。

总之,因为 IoC 容器的存在,使得开发者编写大型系统工程的时候极大地解放了生产力。

DI—Dependency Injection,即“依赖注入”

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

理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:

●谁依赖于谁:当然是应用程序依赖于IoC容器;

●为什么需要依赖:应用程序需要IoC容器来提供对象需要的外部资源;

●谁注入谁:很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;

●注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

举一个DI的例子

在外部(IoC 容器)赋值给它,这个赋值的动作有个专门的术语叫做注入(injection),需要注意的是在 IoC 概念中,这个注入依赖的地方被称为 IoC 容器,但在依赖注入概念中,一般被称为注射器 (injector)。

表达通俗一点就是:我不想自己实例化依赖,你(injector)创建它们,然后在合适的时候注入给我吧。

再比如顾客去餐厅需要碗筷,但是顾客不需要自己带碗筷去,所以,在点菜的时候和服务员说,你给我一副碗筷吧。在这个场景中如果按照正常的编程方式,碗筷本身是顾客的依赖,但是应用 IoC 模式之后 ,碗筷是服务员提供(注入)给顾客的,顾客不用关心吃饭的时候用什么碗筷,因为吃不同的菜品,可能餐具不同,吃牛排用刀叉,喝汤用调羹,虽然顾客就餐时需要餐具,但是餐具的配置应该交给餐厅的工作人员。

这里写图片描述

如果以软件角度来描述,餐具是顾客是依赖,服务员给顾客配置餐具的过程就是依赖注入。

上一节的外卖员和菜品的例子,其实也是依赖注入的例子。

实现依赖注入有 3 种方式:
1. 构造函数中注入
2. setter 方式注入
3. 接口注入

我们现在一一观察这些方式

构造函数注入

public class Person {
 
 
    private Driveable mDriveable;
 
    public Person(Driveable driveable) {
 
        this.mDriveable = driveable;
    }
 
    public void chumen() {
        System.out.println("出门了");
 
        mDriveable.drive();
    }
 
}

优点:在 Person 一开始创建的时候就确定好了依赖。
缺点:后期无法更改依赖。

setter 方式注入

public class Person {
 
    private Driveable mDriveable;
 
    public Person() {
 
    }
 
    public void chumen() {
        System.out.println("出门了");
 
        mDriveable.drive();
    }
 
 
    public void setDriveable(Driveable mDriveable) {
        this.mDriveable = mDriveable;
    }
 
}

优点:Person 对象在运行过程中可以灵活地更改依赖。
缺点:Person 对象运行时,可能会存在依赖项为 null 的情况,所以需要检测依赖项的状态。

public void chumen() {
    if ( mDriveable != null ) {
        System.out.println("出门了");
        mDriveable.drive();
    }
 
    }

接口方式注入

public interface DepedencySetter {
    void set(Driveable driveable);
}
 
class Person implements DepedencySetter{
    private Driveable mDriveable;
 
    public void chumen() {
 
        if ( mDriveable != null ) {
            System.out.println("出门了");
            mDriveable.drive();
        }
 
    }
 
    @Override
    public void set(Driveable driveable) {
        this.mDriveable = mDriveable;
    }
 
}

这种方式和 Setter 方式很相似。有很多同学可能有疑问那么加入一个接口是不是多此一举呢?

答案肯定是不是的,这涉及到一个角色的问题。还是以前面的餐厅为例,除了外卖员之外还有厨师和服务员,那么如果只有外卖员实现了一个送外卖的接口的话,那么餐厅配餐的时候就只会把外卖配置给外卖员。

接口的存在,表明了一种依赖配置的能力。

在软件框架中,读取 xml 配置文件,或者是利用反射技术读取注解,然后根据配置信息,框架动态将一些依赖配置给特定接口的类,我们也可以说 Injector 也依赖于接口,而不是特定的实现类,这样进一步提高了准确性与灵活性。

IoC和DI由什么关系呢?

其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IoC 而言,“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。

你可能感兴趣的:(理解Spring的依赖倒置(DIP)、控制反转(IOC)和依赖注入(DI))