先扯两句
很久没有写设计模式了,依赖倒置原则其实在前两篇发完之后,其实就写了一部分,只是后来由于一些例子的贴切程度和理解上的问题,写到一半,是在不知道后面应该怎么写了。这也就可能导致大家在看的时候,或许有些理解上的困扰,这里先跟大家道个歉。
但是如果不推进下去,那么设计模式就将永远终止在这里,所以我决定还是先将这篇发出来,大家有什么不懂的或者建议可以提出来,后期会重新回来整理依赖倒置原则。
还是先发《设计模式》——目录,然后让我们进入正题。
定义
新的补充
最近看了其他的一些文章,发现自己之前写的太过于生硬了,也就是说并没有理解什么是依赖倒置原则,只是所谓的“理解”了书中所描述的概念而已,其实这种所谓的“理解”理解才是最可怕的,死记硬背的东西,只要一细聊,就全暴露出来了。
这里先补充一下新理解的定义吧:那就是简洁一句话,提倡面向接口编程,再不济也是面向抽象方法编程。
当编程的时候,我们必然需要封装很多的类,无论是服务类、数据存储类、工具类……而当我们封装了这些类以后,在使用的过程中,多多少少的会发现其中有那么一两点不符合我们当前需求的内容。
这里举例说明一下:
不知道大家对于地球的形状有什么认知,无论球还是椭球,但是至少从小就说的一点大家应该能达成共识吧,那就是地球是圆的。可想来大家都知道前段时间新闻报的吧,我这里找的一篇还算中性的文章吧:为证“地球是个平面”,64岁老人自制火箭坠亡
这里不是科学博客,不论证谁对谁错,只是借由这个故事,来告诉大家这个如此多人们认同的事情,也是有人存在其他想法。更别说我们这些天天打码的程序猿(程序媛)了,想必大家都是有这么一个共识的, 那就是被人写的代码都是“垃圾”!所以一旦看对方封装的那个类不爽了,随手就改了。可项目都已经运行了这么久了,鬼知道是不是什么倒霉需求让这个打码不得不写的这么垃圾。而类的代码改变后,又很难有联动提示,因此代码优化一时爽,适配的坑却不知道要花多久来填。
而接口则不同,一旦调整了,则所有使用到改接口的地方都会有联动报错,因此,也能通过这些联动找出具体是哪里出错误,避免盲目修改导致的业务异常。
当然,如上只是针对特定情况的分析,算是一个概念性的解释,具体为什么要这么写,大家还是从下面的读书笔记中寻找答案吧。、
读书笔记
想知道“依赖倒置原则”是什么意思,我感觉首先我们需要知道究竟什么是依赖,如果大家看了我的“里氏替换原则”读后感后,其中有一篇附录,其中就阐述了什么是依赖:
假设A类的变化引起了B类的变化,则说名B类依赖于A类。
当然,这个定义算是比较浅显的解释了,如果想看对应的例子,大家可以自行谷歌百度,当然,也可以看看我的那篇附录——Java的依赖、关联、聚合和组合(这里是不是也体现了封装的重要性,如果附录写在每篇文章后面,这里还得重新贴一遍)
High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.
看过这段“依赖倒置原则”绕口令一样的定义不知道诸位什么感想,反正我是啥都不敢想了,这都是啥啊!!!
还好,书中紧接着就给了关于这段定义的总结:
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象
- 抽象不应该依赖细节
- 细节应该依赖抽象
高层模块和低层模块
至于什么是高层模块?什么是低层模块?书中自然有对应的解释,但是为了体现这篇文章是我自己写的,也不能什么都照搬人家的内容不是,我这里就用自己的理解来解释一下,当然,举例没有人家的简练。
解释之前大家先来回答一个问题:
在这里插入图片描述
想必这个答案现在应该全国人民都是知道的吧,那就不废话了,直接公布答案——三步
- 把冰箱门打开
- 把大象放进去
- 把冰箱门带上
这三步就可以理解为低层模块,而我们要把大象装进冰箱这件事就是高层模块。用一个个的底层模块去拼接组装,最后产生的那个结果就是高层模块。当然,依照书中的说法是:
每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是低层模块
但是从而个人的理解底层模块和高层模块都是相对而言的,并不存在什么绝对不可分割的原子逻辑。或者有些时候,我们分割到某一步的时候,就可以说这里就是底层逻辑了,而不需要深究到最根上。当然这是一个菜鸟的偷懒做法,大家看看就好。
抽象和细节
还是直接上书中的引用吧,对于专业的定义,作为一个懒人我实在是不会编了。。。
在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是可以直接被实例化,也就是可以加上一个关键字new产生一个对象
- 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的
- 接口或抽象类不依赖于实现类
- 实现类依赖接口或抽象类
依赖倒置的好处
依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的
风险,提高代码的可读性和可维护性。
这里还是使用各个装大象的问题,如果想要封装一个可以自己进冰箱的大象就需要他能完成这三步操作:
首先我们需要一个能装大象的冰箱:
class Refrigerator {
private Elephant elephant;
private boolean isDoorOpen = false;
boolean putIn(Elephant elephant) {
if (isDoorOpen) {
this.elephant = elephant;
return true;
}
return false;
}
boolean putOut() {
if (isDoorOpen) {
this.elephant = null;
return true;
}
return false;
}
boolean isDoorOpen() {
return isDoorOpen;
}
void setDoorOpen(boolean doorOpen) {
isDoorOpen = doorOpen;
}
}
然后要一头大象
class Elephant {
}
最后就需要我们将一头大象装进冰箱了。
class Person {
private Refrigerator refrigerator = new Refrigerator();
public void box() {
if (!refrigerator.isDoorOpen()) {
refrigerator.setDoorOpen(true);
}
refrigerator.putIn(new Elephant());
refrigerator.setDoorOpen(false); animal.getClass().getSimpleName());
}
public void showMyAnimal() {
System.out.println("我的冰箱里想着装着的是:" + refrigerator.getAnimal().getClass().getSimpleName());
}
}
@Test
public void boxing(){
new Person()
.box(new Elephant())
.showMyAnimal();
}
运行查看结果:
冰箱里是大象看似我们完成了任务,可是如果老板的需求发生了调整该怎么办呢???
关于这部分我们再来看另一个灵魂问题,那就是把长颈鹿装冰箱需要几步?
在这里插入图片描述
这个就比把大象装冰箱要复杂好多了,因为它竟然需要分四步,整整比装大象多了一步!!!:
- 把冰箱门打开
- 把大象拿出来
- 把长颈鹿放进去
- 把冰箱门带上
而在将这个需求套到刚刚我们封装好的冰箱里,你就会发现,我们的箱子只能装大象,竟然装不了长颈鹿,这不科学啊!我们总不能为了装长颈鹿,再专门做一个只能装长颈鹿的冰箱,这样下去,大家回家打开自己的冰箱看看,那么多东西都非成不同的冰箱去装,那得多么大的房子才行。
所以,最根本的原因不是家太小,而是这个冰箱设计的不科学,套用作者的话,就是:
冰箱类与大象类是紧耦合的关系,其导致的结果就是系统的可维护性大大降低,可读性降低。
我们需要做的就是让这个冰箱在能装大象的同时,也能装长颈鹿。
既然想做这么一个冰箱,我们首先要知道的就是大象与长颈鹿有什么共同点,很显然他们都是动物,这里我们只需要创建一个动物了
interface Animal {}
然后需要的就是我们的大象和长颈鹿了
class Elephant implements Animal { }
class Giraffe implements Animal { }
我们需要将冰箱做一些调整:
class Refrigerator {
private Animal animal;
private boolean isDoorOpen = false;
boolean putIn(Animal animal) {
if (isDoorOpen && !hasAnimal()) {
this.animal = animal;
return true;
}
return false;
}
boolean putOut() {
if (isDoorOpen && hasAnimal()) {
this.animal = null;
return true;
}
return false;
}
private boolean hasAnimal() {
return null != animal;
}
boolean isDoorOpen() {
return isDoorOpen;
}
void setDoorOpen(boolean doorOpen) {
isDoorOpen = doorOpen;
}
}
这个时候我们就可以完成装大象和长颈鹿的操作了
class Person {
private Refrigerator refrigerator = new Refrigerator();
public Person box(Animal animal) {
if (!refrigerator.isDoorOpen()) {
refrigerator.setDoorOpen(true);
}
if (refrigerator.hasAnimal()) {
refrigerator.putOut();
}
refrigerator.putIn(animal);
refrigerator.setDoorOpen(false);
return this;
}
public void showMyAnimal() {
System.out.println("我的冰箱里想着装着的是:" + refrigerator.getAnimal().getClass().getSimpleName());
}
}
@Test
public void boxing() {
new Person()
.box(new Elephant())
.box(new Giraffe())
.showMyAnimal();
}
运行查看结果:
冰箱里是长颈鹿所以这里的启示就是:
设计是否具备稳定性,只要适当地“松松土”,观察“设计的蓝图”是否还可以茁壮
地成长就可以得出结论,稳定性较高的设计,在周围环境频繁变化的时候,依然可以做到“我自岿然不动”
再进一步
当然,实际项目中的需求可能远远不止上面说的这么简单,想必你们也同我一样,是善良的,怎么忍心所有动物都装进冰箱里,若非要装在哪里,我们暂时还是把大象装进动物园的笼子里吧。
而且这里我们是泛指的Person去执行的装这个操作,但如果我们是特定的人才能执行这个装的操作呢?下面我再次调整一下冰箱、大象、和人,再来看一下效果。
动物:
interface Animal {
}
class Elephant implements Animal {
}
class Giraffe implements Animal {
}
箱子:
abstract class Box {
private Animal animal;
private boolean isDoorOpen = false;
protected boolean putIn(Animal animal) {
if (isDoorOpen && !hasAnimal()) {
this.animal = animal;
return true;
}
return false;
}
protected boolean putOut() {
if (isDoorOpen && hasAnimal()) {
this.animal = null;
return true;
}
return false;
}
protected boolean hasAnimal() {
return null != animal;
}
protected boolean isDoorOpen() {
return isDoorOpen;
}
protected void setDoorOpen(boolean doorOpen) {
isDoorOpen = doorOpen;
}
protected Animal getAnimal() {
return animal;
}
abstract Box boxing(IPerson person, Animal animal);
}
class Refrigerator extends Box {
private List<String> userName;
{
userName = new ArrayList<>();
userName.add("张三");
userName.add("李四");
userName.add("王二");
}
@Override
Box boxing(IPerson person, Animal animal) {
if (userName.contains(person.getName())) {
if (!isDoorOpen()) {
setDoorOpen(true);
}
if (hasAnimal()) {
putOut();
}
putIn(animal);
setDoorOpen(false);
System.out.println(person.getName() + " 在冰箱中装入了 " + animal.getClass().getSimpleName() + " 现在冰箱中的动物是 " + getAnimal().getClass().getSimpleName());
} else {
System.out.println(person.getName() + " 无权限在冰箱中装 " + animal.getClass().getSimpleName() + " 现在冰箱中的动物是 " + getAnimal().getClass().getSimpleName());
}
return this;
}
}
class ZooCage extends Box {
@Override
Box boxing(IPerson person, Animal animal) {
if (!isDoorOpen()) {
setDoorOpen(true);
}
if (hasAnimal()) {
putOut();
}
putIn(animal);
setDoorOpen(false);
System.out.println(person.getName() + "在动物园的笼子中装入了" + animal.getClass().getSimpleName() + "现在动物园的笼子中的动物是 " + getAnimal().getClass().getSimpleName());
return this;
}
}
人:
interface IPerson {
String name = null;
public String getName();
}
class Person implements IPerson {
private String name;
public Person(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
这个时候我们再执行一下添加的操作:
@Test
public void boxing() {
IPerson zhangSan = new Person("张三");
IPerson liSi = new Person("李四");
IPerson maZi = new Person("麻子");
Animal elephant1 = new Elephant();
Animal giraffe = new Giraffe();
Animal elephant2 = new Elephant();
Box refrigerator = new Refrigerator();
Box zooCage = new ZooCage();
refrigerator.boxing(zhangSan, elephant1)
.boxing(liSi, giraffe)
.boxing(maZi, elephant2);
zooCage.boxing(zhangSan, elephant1)
.boxing(liSi, giraffe)
.boxing(maZi, elephant2);
}
运行结果:
运行结果可以看到,这样箱子、人、动物之间耦合将降到足够低,当Person,或者Animal中有所调整的时候,对已编写代码造成的影响尽可能小。例如:
- 这里的冰箱,我们增加了动物园中箱子ZooCage,并没有对人或者是动物产生影响
- 在冰箱中添加人的识别,只需要调整人IPerson与Person,而不会对动物产生影响,同时也不会对另一个Box Refrigerator造成影响。
也就是说修改了高层模块,而底层的Elephant、Giraffe则不需要调整。
把“变更”引起的风险扩散降到最低
另外,这样封装,我们就可以解决并行开发的问题,只要相互之间约定好接口,就都可以启动开发任务,并可以各自进行单元测试,提升开发的效率和质量。甚至有必要的时候可以先编写伪接口,便于多方的工作可以正常进行下去,真正实现了这个部分的功能后,再重新联调一下即可。
在Java中,只要定义变量就必然要有类型,一个变量可以有两种类型:表面类型
和实际类型,表面类型是在定义的时候赋予的类型,实际类型是对象的类型
其中elephant1的表面类型是Animal,而实际类型则是Elephant。
总结与用法
依赖可以传递
例如Android中:
MyActivity(我们创建用于展示的页面)-->AppCompatActivity-->FragmentActivity-->SupportActivity-->Activity-->ContextThemeWrapper-->ContextWrapper-->Context
依赖倒置本质
依赖倒置原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合。
依赖倒置实现规则
- 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备
- 变量的表面类型尽量是接口或者是抽象类
不是只能使用最底层的抽象类或接口,毕竟如果真的这样,至少从java的角度,一大部分实体类都会使用Object,而这显然是不现实的。因此主要是看当前的业务逻辑中的调用使用的是什么层级的方法。如上面的例子,接收的是Animal,因此我们大象和长颈鹿的表面类型都是Animal。可如果我们使用的冰箱依然是之前所使用的只能装大象的冰箱,表面类型再使用Animal就会报错。但在通常情况下,我们还是尽可能使用尽可能底层的接口或抽象类,便于维护阅读以及归纳。
- 任何类都不应该从具体类派生
不是绝对,熟悉Android的朋友可能知道Activity是早期的基本组件,因此是具体类,但后续对于Activity的优化必然是继承自Activity的,因此维护的时候,难免有直接从具体类派生的情况,即便是google这样的大公司都有这种操作,更别说我们了。但是这不是我们随意使用实体类派生的借口,而是应该在架构的时候尽可能避免这种情况。
- 尽量不要覆写基类的方法
这里需要先说明一点,这里的不要覆写是指的从继承的依赖倒置角度,而不是使用接口的回调、监听等情况,因为那样做的目的就是为了去覆写。
之后在来看继承的时候为何避免覆写,正如书中所写“覆写了抽象方法,对依赖的稳定性会产生一定的影响”。因为覆写会改变父类原有的实现逻辑,如果大家对抽象方法的实现逻辑已经达成了共识。其中一个环节做出调整后,后续的依赖关系都会受到影响,而且维护的时候,从众多层级的继承关系中,找到具体是哪里做了修改,也是很困难的。建议一旦真的有需要的时候,可以重新写一个新的方法,调用这个抽象方法,之后再实现新的逻辑。如有必要可以为原抽象方法在这次覆写中添加@deprecated,标注在当前依赖的分支中,该方法已经不再试用。
- 结合里氏替换原则使用
里氏替换原则,父类出现的地方子类就能出现,再结合本章的讲解,我们可以得出这样一个通俗的规则:接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。
总结
依赖倒置的优点主要是减少后期维护时造成的影响,毕竟唯一不变的就是变化,因此当设计优良、代码结构清晰的时候,维护人员可以花费尽可能小的成本了解现有程序,减低人员变动对工程造成的影响。
网友评论