现在我们来学习依赖倒置原则,首先我们来看一下他的定义,高层模块不应该依赖底层模块,两者都应该依赖其抽象,
抽象不应该依赖细节,细节应该依赖于抽象,针对接口编程,而不要针对实现编程,那我们使用抽象包括引用接口,
或者抽象类,可以使各个类,模块的实现彼此独立,不影响,从而实现模块间的松耦合,降低模块间的耦合性,那我们在使用
依赖倒置原则的时候,还有一些要注意的点
比如每个类尽量继承自接口,或抽象类,这里面就说的比较泛化,也就是说你可以通过implements,实现接口,
通过一个extends继承一个抽象类,或者继承抽象类继承接口,这个只是说尽量,尽量避免具体的类派生,
尽量覆盖其方法,可以减少类间的耦合性,提高系统的稳定性,提高代码的可读性,和可维护性,并且最重要的,
还可以降低程序的风险,那依赖倒置原则也非常的简单,我们通过coding就可以非常容易的理解他了,
那现在我们一起来coding吗,那我们先创建一个包,主要目的是降低耦合,首先我们有这样一个场景,
我学习,可能对JAVA的课程比较感兴趣,另外我对前端的课程也比较感兴趣
package com.learn.design.principle.dependenceinversion;
/**
* 我们来实现两个方法
* 我们的问题就是
* 假设我们现在要学习Python的课程
* 那么我们在这里加
* 实体的方法是在不断的补充的
* 我们写一个python
* 这里面我们来体会一下
* 现在我们的做法就是面向实现编程
* 因为整个Geely就是一个实现类
* 我们在面向实现编程
* 会发现一个最重要的问题
* 整个实现类是需要经常修改的
* 扩展性比较差
* 也就是说我们应用层的函数
*
* 我们现在看一下Geely这个类
* 现在这三个具体的实现就可以干掉了
* 我们看一下这个场景
* 大家就比较熟悉了
*
* 当然我们也可以通过构造器的方式来注入
* Geely里面具体的实现
* 然后我们在类里面声明一个iCourse
* 用private也OK
* 在这里是不是很熟悉
* 比如对应的一个注入
* 然后this.iCourse = iCourse赋值
* 这个时候studyImoocCourse就不需要参数了
* 我只要调用类里面的成员变量就可以了
* 这个就是第三个版本了
* 构造器注入
*
* 构造器使用起来还不是太方便
* 如果Geely想学别的
* 我们还需要去new一个Geely
* 因为这个只能在构造的时候才能注入进去
*
*
* @author Leon.Sun
*
*/
public class Geely {
/**
* 现在我们把它开放出去
* setter注入的方式
*
*
* @param iCourse
*/
public void setiCourse(ICourse iCourse) {
this.iCourse = iCourse;
}
private ICourse iCourse;
/**
* 我们实现一个方法
* 我们通过一个方法传一个对象
* 而这个对象是需要用接口的
* 因为我具体传FE课程还是JAVA课程
* 是需要依据高层模块Test
*
*/
public void studyImoocCourse(){
/**
* 这里只需要iCourse调用studyCourse就可以了
* 学习课程的时候
* 具体的实现交给高层的模块
* 而不是根据实现类来编写
*
*
*/
iCourse.studyCourse();
}
}
package com.learn.design.principle.dependenceinversion;
/**
* 我们再来一个FECourse
* 实现ICourse
* 包不要选错
*
*
* @author Leon.Sun
*
*/
public class FECourse implements ICourse {
/**
* 实现他的方法
*
*/
@Override
public void studyCourse() {
System.out.println("Geely在学习FE课程");
}
}
package com.learn.design.principle.dependenceinversion;
/**
* 首先我们创建一个类
* 用接口Icourse
* 还是课程
*
*
* @author Leon.Sun
*
*/
public interface ICourse {
/**
* 这个课程有个方法studyCourse
* 具体学什么课程
* 我们交给高层的应用层来选择
* 接下来也很简单
*
*/
void studyCourse();
}
package com.learn.design.principle.dependenceinversion;
/**
* JavaCourse实现ICourse这个课程类
* 因为在开闭原则里面
* 也使用了同样的类名
* 选择我们刚刚创建的这个类
* 现在实现了这个类
*
*
* @author Leon.Sun
*
*/
public class JavaCourse implements ICourse {
@Override
public void studyCourse() {
System.out.println("Geely在学习Java课程");
}
}
package com.learn.design.principle.dependenceinversion;
/**
* 实现ICourse
* 这样我们就实现了学习Python的一个类
*
* @author Leon.Sun
*
*/
public class PythonCourse implements ICourse {
@Override
public void studyCourse() {
System.out.println("Geely在学习Python课程");
}
}
package com.learn.design.principle.dependenceinversion;
public class Test {
/**
* 我们的问题就是
* 假设我们现在要学习Python的课程
*
* 因为TEST这个类是应用层的
* 它是属于高层模块
* Geely是属于低层模块
* 因为现在只有这两个类
* 根据依赖倒置的原子性
* 高层次的模块是不应该依赖于低层次的模块的
* 也就是说Test里面的实现
* 现在依赖于Geely的具体实现
* 里面实现什么
* 我都要来扩展
* 然后再高层模块才可以使用
* 那我们现在引入抽象
* 看看如何来解决这个问题
*
* 这里认为是一个v1版本
* 放到这儿
* 这样的话也有个对比
*
* @param args
*/
//v1
// public static void main(String[] args) {
// Geely geely = new Geely();
// geely.studyJavaCourse();
// geely.studyFECourse();
// }
/**
* 还是new一个Geely
* 这里面new一个JavaCourse
* 再new一个FECourse
* 输出结果是一样的
* 但是写法是不一样的
* 我们看一下类图
* 首先ICourse接口放在这里边
* 如果有其他课程的实现
* 例如Python的课程
* 和他们平级放在这里面
* 而具体的Geely这个类
* 不需要动的
* 就是说我们要面向接口编程
* 我去写的这个扩展类
* 是面向接口的
* 而不是面向具体的Geely这个实现类
* 对于高层模块
* 具体我们学习什么课程
* 例如Python
* 是让高层模块来选择
* 这样的话就做到了
* Geely和Test两个之间是解耦的
* 同时Geely和课程的具体实现是解耦的
* 但是他和ICourse是耦合的
* 所谓的高内聚和低耦合
* 也就是尽量减少耦合
* 因为有依赖关系
* 例如ICourse是Geely这个类的
* 一个方法中的参数
* 所以他两之间是有关系的
* 所以看到这个类图我们就想
* 接下来的扩展就非常简单了
* 例如我现在还想学习Python的课程
* 我们来创建爱一个类
* 而对于高层模块
* 应用层的Test类
* 我们通过geely.studyImoocCourse
* 里面的参数注入进去
* 这样的话我们看一下结果
* 对于新的课程
* 我们只需要新建一个类
* 然后实现这个接口
* 面向接口编程
* 然后具体实现哪个课程
* 交给应用层的Test类
* 高层模块
* 这个是通过接口方法的方式
* 来注入具体的实现
*
* v2是接口方法注入的方式
*
*
*
*
* @param args
*/
//v2
// public static void main(String[] args) {
// Geely geely = new Geely();
// geely.studyImoocCourse(new JavaCourse());
// geely.studyImoocCourse(new FECourse());
// geely.studyImoocCourse(new PythonCourse());
// }
/**
* Geely中new一个Geely
* 里面new一个JavaCourse
* geely.studyImoocCourse
* 这里就开始学课程了
* 是因为具体的ICourse的实现
* 构造器已经注入了
* 这是第三个版本
*
* 构造器使用起来还不是太方便
* 如果Geely想学别的
* 我们还需要去new一个Geely
* 因为这个只能在构造的时候才能注入进去
* 因为是单例模式
* 如果在这里面还想学FE课程
* 现在只能new一个Geely出来
* 因为Geely类里面并没有对ICourse的一个注入
*
*
* @param args
*/
//v3
// public static void main(String[] args) {
// Geely geely = new Geely(new JavaCourse());
// geely.studyImoocCourse();
// }
/**
* 接下来我们接着写他的实现
* 这样就不依赖于自己的构造器了
* 也就是Geely在具体执行的时候
* 学习课程
* 他不需要关心
* 也就是这个方法不需要关心
* 我学A的我学B的
* 比如第一种写法我要关心具体的实现
* 我要调用哪个方法呢
* 我要调用学习JAVA的方法
* 我要调用学习前端课程的方法
* 输出结果也是一样的
* 我们再看一下现在的类图
*
* @param args
*/
public static void main(String[] args) {
Geely geely = new Geely();
geely.setiCourse(new JavaCourse());
geely.studyImoocCourse();
/**
* 学习JAVA课程之后
* 我还想学习前端的课程
*
*
*/
geely.setiCourse(new FECourse());
geely.studyImoocCourse();
}
}
我们可以看到三个具体的实现,实现ICourse,Geely在这里面,应用层的类在这里面,底层模块和高层模块,不是说在
上面就是高,而是按照应用层,MVC架构里边,对上层我们接收请求的,可能是Controller,他下一层叫做Service层,
Service层去调用DAO层,service层相对于DAO层是高的,这里面对外的应用层肯定是Test,所以这里面我们要体会一下,
面向接口编程,和面向实现编程的最大区别,相信通过这个例子,我们对依赖倒置原则有深刻的理解,当然我们这里不需要
接口,用抽象类也可以,那抽象类也是一种抽象,Geely这个类是不依赖于具体的,是JavaCourse,还是FECourse,还是Python
Course,我们可以看到我们在Geely这个类里面根本没有导入他们的包,那其实我们在体会的时候呢,可以看import,
当然现在都处于同一个包下,不需要import,直接就能够引用过来,但是这个类里面并没有显性的去写JavaCourse,
PythonCourse,FECourse,也就是Geely想学什么课,我们都可以在不动Geely这个模块的前提下,任意的修改,
Test是应用层的,肯定是需要改的,主要是Geely,Geely是相对于ICourse,实现更高层的一个类,具体想学什么课,
这个类都不需要动,我只需要在底层模块进行扩展,注意扩展,我学什么课,不会修改JavaCourse,FECourse,和PythonCourse,
比如我先做学算法课,这个三个类是不需要动的,这也符合开闭原则,不能说我学个算法课,要在JavaCourse的实现里边,
各种写,很容易引入新的问题,依赖倒置的原则就是,高层次的模块不应该依赖于低层次的模块,那依赖倒置的原则,
表达出一个什么样的事实呢,抽象的东西要稳定的多,以抽象为基础,搭建起来的架构,比以细节搭建起来的,抽象的
目的就是制定规范,比如ICourse的一个接口,学习一个课程是一个契约,具体怎么实现交给具体的实现类,而Geely是
不依赖于具体课程的实现,因为现在是这几个种类,说不定,又出现什么新的语言了,那Geely这个类不需要动,
其他课程实现类也不需要动,只要新增实现就可以了,所以核心思想就是面向接口编程,一定要记住,面向接口编程,
那理解依赖倒置原则,理解Spring的依赖注入和控制反转,那就相当容易了,我们再看一下类图,希望理解依赖倒置原则