不诗意的女程序媛不是好厨师~
转载请注明出处,From李诗雨---https://blog.csdn.net/cjm2484836553/article/details/104449190
上篇我们学习了注解的基本知识,今天我们再来学习一下依赖注入。
当让我们也不是一下子就来说依赖注入,而是秉着循序渐进的原则,一点一点的理解。
那就让我们开始吧~
1.什么是依赖(Dependency)?
通俗点来讲 依赖 就是一种需要。
依赖是类与类之间的连接,依赖关系表示一个类依赖于另一个类的定义。
【举个栗子】:
例如一个人(Person)可以买车(Car)和房子(House),那Person类就依赖于Car类和House类。
public static void main(String[] args) {
Person person = new Person();
person.buy(new House());
person.buy(new Car());
}
static class Person {
//表示依赖House
public void buy(House house) {
System.out.println("买了套房");
}
//表示依赖Car
public void buy(Car car) {
System.out.println("买了辆车");
}
}
static class House {
}
static class Car {
}
嗯呐,了解了依赖是什么,接下来继续下一个概念:依赖倒置 ~
2.什么是 依赖倒置 ?
2.0 顺序依赖
有时候理解一个东西,通过反面来理解也不失为一种好的方法。
那依赖倒置的反面就是不依赖倒置了,就是顺序依赖了。
什么是顺序依赖呢?
比如说:
人的出行依赖于车子,那如果出行由自行车变成了汽车,人对应的类就要改变;如果又变成了火车,人对应的类又要改变。
人依赖于车子,车子一变,人就要跟着改变,这种就叫顺序的依赖。
public class Person {
private Bike mBike;
private Car mCar;
private Train mTrain;
public Person() {
mBike = new Bike();
//mCar = new Car();// ①变---改为汽车
//mTrain = new Train();//②变---改为火车
}
public void goOut() {
System.out.println("依赖于车子要出门了~");
mBike.drive();
//mCar.drive(); //①跟着变
//mTrain.drive(); //②跟着变
}
public static void main(String... args) {
Person person = new Person();
person.goOut();
}
}
好的,了解了顺序的依赖,那依赖的倒置就是反过来喽,那就让我再来继续看看吧~
2.1依赖倒置的定义
依赖倒置是面向对象设计领域的一种软件设计原则。
依赖倒置原则(Dependence Inversion Principle,简称DIP)
- 核心思想:高层模块不应该依赖底层模块,二者都该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象;
- 说明:高层模块就是调用端,低层模块就是具体实现类。抽象就是指接口或抽象类。细节就是实现类。
- 通俗来讲: 依赖倒置原则的本质就是通过抽象(接口或抽象类)使个各类或模块的实现彼此独立,互不影响,实现模块间的松耦合。
- 问题描述: 类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
- 解决方案: 将类A修改为依赖接口interface,类B和类C各自实现接口interface,类A通过接口interface间接与类B或者类C发生联系,则会大大降低修改类A的几率。
- 好处:依赖倒置的好处在小型项目中很难体现出来。但在大中型项目中可以减少需求变化引起的工作量。使并行开发更友好。
我们现在知道了依赖倒置原则的核心是:
- 上层模块不应该依赖底层模块,它们都应该依赖于抽象。
- 抽象不应该依赖于细节,细节应该依赖于抽象。
那什么是上层模块和底层模块 ,什么又是抽象和细节呢?
我们来继续往下看~
2.2上层模块和底层模块
就拿一个公司来说吧,它一定有架构的设计、有职能的划分。按照职能的重要性,自然而然就有了上下之分。并且,随着模块的粒度划分不同这种上层与底层模块会进行变动,也许某一模块相对于另外一模块它是底层,但是相对于其他模块它又可能是上层。
如图,公司管理层就是上层,CEO 是整个事业群的上层,那么 CEO 职能之下就是底层。
然后,我们再以事业群为整个体系划分模块,各个部门经理以上部分是上层,那么之下的组织都可以称为底层。
由此,我们可以看到,在一个特定体系中,上层模块与底层模块可以按照决策能力高低为准绳进行划分。
那么,映射到我们软件实际开发中,一般我们也会将软件进行模块划分,比如业务层、逻辑层和数据层。
业务层中是软件真正要进行的操作,也就是做什么。
逻辑层是软件现阶段为了业务层的需求提供的实现细节,也就是怎么做。
数据层指业务层和逻辑层所需要的数据模型。
因此,如前面所总结,按照决策能力的高低进行模块划分。业务层自然就处于上层模块,逻辑层和数据层自然就归类为底层。
2.3抽象和具体
抽象如其名字一样,是一件很抽象的事物。
抽象往往是相对于具体而言的,具体也可以被称为细节。
比如:
1.交通工具是抽象,而公交车、单车、火车等就是具体了。
2.表演是抽象,而唱歌、跳舞、小品等就是具体。
由此可见,抽象可以是物也可以是行为。
在我们实际的软件开发中,抽象有两种形式:接口 & 抽象类。
/**
* Driveable 是接口,所以它是抽象
*/
public interface Driveable {
void drive();
}
/**
* 而 Bike 实现了接口,它被称为具体。
*/
public class Bike implements Driveable {
@Override
public void drive() {
System.out.println("Bike drive");
}
}
2.4依赖倒置的好处
在上述的人与车子的例子中,只要车子的类型一变,Person类就要跟着改动。
那有没有一种方法能让 Person 的变动少一点呢?
因为毕竟我们写的是最基础的演示代码,如果工程大了,代码复杂了,Person 面对需求变动时改动的地方会更多。
而依赖倒置原则正好适用于解决这类情况。
下面,我们尝试运用依赖倒置原则对代码进行改造。
我们再次回顾下它的定义。
上层模块不应该依赖底层模块,它们都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。
首先是上层模块和底层模块的拆分。
按照决策能力高低或者重要性划分,Person 属于上层模块,Bike、Car 和 Train 属于底层模块。
上层模块不应该依赖于底层模块。
public class Person {
//======顺序的依赖======
//private Bike mBike;
//private Car mCar;
//private Train mTrain;
//=====依赖倒置=====
private Driveable mDriveable;
public Person() {
//======顺序的依赖======
//mBike = new Bike();
//mCar = new Car();// ①变---改为汽车
//mTrain = new Train();//②变---改为火车
//=====依赖倒置=====
mDriveable = new Train(); //依赖倒置,只需要改这里就可以了,其他地方不用修改了
}
public void goOut() {
System.out.println("依赖于车子要出门了~");
//======顺序的依赖======
//mBike.drive();
//mCar.drive(); //①跟着变
//mTrain.drive(); //②跟着变
//=====依赖倒置=====
mDriveable.drive();
}
public static void main(String... args) {
Person person = new Person();
person.goOut();
}
}
可以看到,依赖倒置实质上是面向接口编程的体现。
好的,通过上面的讲解相信你已经渐渐体会到什么是依赖倒置了。
但是,在编码的过程中,我们还是发现它的一个不足,那就是它不符合开闭原则。
每次我们都要修改Person类的内部,那我们能不能再不修改Person类内部的情况下,来实现同样的功能呢?
答案当时是肯定的,所以接下来我们再来学习一下 控制反转。
3.什么是控制反转?
控制反转( IoC )是 Inversion of Control的缩写,意思就是对于控制权的反转。
上面的实例中,Person自己掌控着内部 mDriveable 的实例化。
现在,我们可以更改一种方式。将 mDriveable 的实例化移到 Person 外面。
public class Person2 {
private Driveable mDriveable;
public Person2(Driveable driveable) {
this.mDriveable = driveable;
}
public void goOut() {
System.out.println("出门啦");
mDriveable.drive();
}
public static void main(String... args) {
Person2 person = new Person2(new Car());//变为了在Person的外部进行改变
person.goOut();
}
}
就这样无论出行方式怎么变化,Person 这个类都不需要更改代码了。
在上面代码中,Person 把内部依赖的创建权力移交给了 Person2这个类中的 main() 方法。也就是说 Person 只关心依赖提供的功能,但并不关心依赖的创建。
这种思想其实就是 IoC,IoC 是一种新的设计模式,它对上层模块与底层模块进行了更进一步的解耦。控制反转的意思是反转了上层模块对于底层模块的依赖控制。
好了,学习了这么多的概念,现在我们终于有了一定的基础来学习依赖注入了~
4.什么是依赖注入
依赖注入,也经常被简称为 DI,其实在上一节中,我们已经见到了它的身影。它是一种实现 IoC 的手段。什么意思呢?
为了不因为依赖实现的变动而去修改 Person,也就是说以可能在 Driveable 实现类的改变下不改动 Person 这个类的代码,尽可能减少两者之间的耦合。我们需要采用上一节介绍的 IoC 模式来进行改写代码。
这个需要我们移交出对于依赖实例化的控制权,那么依赖怎么办?Person 无法实例化依赖了,它就需要在外部(IoC 容器)赋值给它,这个赋值的动作有个专门的术语叫做注入(injection),需要注意的是在 IoC 概念中,这个注入依赖的地方被称为 IoC 容器,但在依赖注入概念中,一般被称为注射器 (injector)。
表达通俗一点就是:我不想自己实例化依赖,你(injector)创建它们,然后在合适的时候注入给我。
实现依赖注入有 3 种方式:
- 构造函数中注入
- setter 方式注入
- 接口注入
/**
* 接口方式注入
* 接口的存在,表明了一种依赖配置的能力。
*/
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();
}
public static void main(String... args) {
Person2 person = new Person2(new Car());
person.goOut();
}
}
所以,依赖注入大家要记住的重点就是 : 依赖注入是实现控制反转的手段。
积累点滴,做好自己~