楼主

56515发表于 2019-11-28 09:20:36
只看该作者楼主

【综合】【码手设计-1】依赖倒置原则 [复制链接]

作者:HW-HM

先看一个简单的输入输出程序是如何在需求增加中一步一步变烂的。

[code lang='java']
/****************************************************************
* 版本1:要求编写一个从键盘读入字符并输出到打印机的程序
* Read Keyboard (char) -> Copy -> (char) Writer Printer
****************************************************************/
public class Copier {
public static void Copy() {
int c;
while ((c = Keyboard.Read()) != -1) {
Printer.Writer(c);
}
}
}
/****************************************************************
* 版本2: 需求变更:支持从纸带读入机中读入信息?
****************************************************************/
public class Copier {
// remember to reset this flag
public static boolean ptFlag = false;
public static void Copy() {
int c;
while ((c = (ptFlag ? PaperTape.Read() : Keyboard.Read())) != -1) {
Printer.Writer(c);
}
}
}
/****************************************************************
*版本3: 需求变更:支持输出到纸带穿孔机上?
****************************************************************/
public class Copier {
// remember to reset thess flags
public static boolean ptFlag = false;
public static boolean punchFlag = false;
public static void Copy() {
int c;
while ((c = (ptFlag ? PaperTape.Read() : Keyboard.Read()))!= -1) {
punchFlag ? PaperTaper.Punch() : Printer.Writer(c);
}
}
}
/****************************************************************
*版本4: 需求变更:支持从多种输入设备读取信息

****************************************************************/
public interface Reader {
int Read();
}
public class KeyboardReader implements Reader {
public int Read() {return Keyboard.Read();}
}
public class Copier {
public static Reader reader = new KeyboardReader();
public static void Copy() {
int c;
while ((c = reader.Read())) != -1) {
Printer.Write(c);
}
}
}
[/code]

版本2和版本3都通过增加bool变量来指明对应的输入输出来源,其对应的Copy函数依赖于每一种输入输出的具体实现。每增加一种输入输出实现,Copy函数就得修改;输入输出的实现修改了, Copy函数所在文件也需要重新编译。因此Copy函数是很不稳定的,因为它依赖于具体实现了;同时Copy函数也很难重用,因为必须将其依赖的那些输入输出实现全部包含进来,而这些输入输出的实现却不能用其他的实现进行替换。

版本4抽象出了Reader接口,使得增加一种新的输入时Copy函数不需要修改。但这里的Copier类仍依赖于Reader的具体实现KeyboardReader,有两种方法可以解决这个问题,一种方法是将Reader作为Copy的参数输入,由更上层的逻辑决定实例化哪个具体的Reader,另一种方法是将Reader的实例化独立出去,由另外的类(通常是工厂类)决定如何实例化。经过这样的修改后的Copier对输入的增加就能免疫了,无需进行任何修改,因为只依赖于Reader接口抽象。这就是面向对象设计的依赖倒置原则(DIPdependence inversion principle),其描书如下:

抽象不应该依赖于实现细节,实现细节应该依赖于抽象。

高层模块不应该依赖于低层模块,二者都应该依赖于抽象。

要针对接口编程,不针对实现编程。

再回顾一下我们的例子,Copier在这里就是一个高层模块(逻辑),其中的Copy函数从输入读一个字符,然后向输出写一个字符就是实现细节,这些都应该依赖于抽象的Reader

编程时使用接口和抽象类进行变量类型声明、参数类型声明、方法返还类型说明,以及数据类型的转换等。而不要用具体类进行变量的类型声明、参数类型声明、方法返还类型说明,以及数据类型的转换等。要保证做到这一点,一个具体类应当只实现接口和抽象类中声明过的方法,而不要给出多余的方法。

从复用的角度来说,高层次的模块是应当复用的,而且是复用的重点,因为它含有一个应用系统最重要的宏观商务逻辑,是较为稳定的。要使得高层模块可以复用,它不应该依赖于具体的模块或实现,也就是说必须使其满足依赖倒置原则。

再以汽车制造的例子说明一下依赖倒置原则的思想。迈腾是大众的一款汽车,旗下有舒适性、豪华型、运动型等,价格差距相当大,排量也不一样,不过它们的车身大小、底盘结构等都是一样的。汽车的底盘和车身,可以看作是汽车的高层模块,底盘和车身决定了像发动机、轮胎、内饰等这样的低层模块安装到底盘或车身的哪个位置以及如何安装。发动机不是焊接在底盘上的,而是通过几个位置固定的螺丝钉拧上去的,这就是底盘和发动机之间的接口,这使得发动机就可以随意替换,只要发动机满足这个接口,同时这也使得可以对汽车进行改装。舒适性、豪华型、运动型其实就是对这些接口做了具体的替换而已,我不得不佩服造汽车的人,因为他们如此善于使用依赖倒置原则。

如果程序的某个子逻辑会变,那么它不应该是焊接上去的,而应该是通过螺丝拧上去的,这就是依赖倒置原则。知道了这个,在设计时,应该怎么做呢?应该先分析高层逻辑,看要实现这些逻辑必需别人提供哪些东西,并对可能变化的部分抽象出接口(抽象类),然后用具体类去实现这些接口。也就是说分析的顺序应该是这样的:

举报
楼主发表于 2019-11-28 16:45:24
只看该作者沙发

欢迎关注这位老师的其他文章哦

举报
楼主发表于 2019-11-28 16:45:58
只看该作者板凳
举报
楼主发表于 2019-11-28 16:46:11
只看该作者地板
举报
楼主发表于 2019-11-28 16:46:46
只看该作者5 #
举报
楼主发表于 2019-11-28 16:46:53
只看该作者6 #
举报
楼主发表于 2019-11-28 16:47:05
只看该作者7 #
举报
楼主发表于 2019-11-28 16:47:18
只看该作者8 #
举报
楼主发表于 2019-11-28 16:47:26
只看该作者9 #
举报
楼主发表于 2019-11-28 16:47:37
只看该作者10 #
举报

您需要登录后才可以回帖

登录注册
发表回复