概述
(1) 定义
a.高层模块和低层模块的关系:高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
b.抽象和细节的关系:抽象不应该依赖于细节,细节应该依赖于抽象。
(2) 依赖倒置原则案例
许多传统的软件开发方法,比如结构化分析和设计方法,总是倾向于创建一些高层模块依赖于低层模块,策略(policy)依赖于细节的软件结构。
下面用一个例子讲解一下传统的设计是怎样的不灵活,最后我们再用依赖倒置原则对传统的设计进行重构,使其成为一个易于扩展的设计。
编写一个从键盘读入字符并输出到打印机的程序。
传统设计(违反DIP)
UML设计如如图1,伪代码如程序1。
// 程序1,源码program1包。// 主程序public class Copy { ReadKeyboard rdKbd = new ReadKeyboard();// 从键盘读取字符 WritePrinter wrtPrt = new WritePrinter();// 将字符写入打印机 public void copy(){ char c; while((c = rdKbd.read()) != EOF){ wtPrt.write(c); } }}// 从键盘读取字符public class ReadKeyboard { public char read(){ char c = 'k';// 'k' 是从键盘读取到的字符; System.out.println("从键盘读取字符"); return c; }}// 将字符写入打印机public class WritePrinter { public void write(char c){ System.out.println("将字符写入打印机"); }}// 客户端public class Client { public static void main(String[] args) { Copy copy = new Copy(); copy.copy(); }}
程序运行结果:
从键盘读取字符
将字符写入打印机
这样高层依赖于低层的设计是不好的,程序不是一成不变的,后续需求会不断发生变化,需求的变化会迫使我们不断修改高层的Copy程序。一个好的程序设计,在需求变更的时候,可以增加低层模块,或者修改已有的低层模块,最好不要动高层模块,因为当你修改了高层模块,可能已经运行良好的低层模块就会出现问题,“牵一发而动全身”,不利于程序的扩展和维护,违反了开闭原则。
程序成功上线了,运行一段时间之后,业务方提出来这个程序不仅要从键盘读入字符,还要从纸带读入机中读入信息,此时我们只能对Copy类进行修改,在copy方法增加一个boolean类型的入参,当参数为true,来对读取信息的设备做判断,当参数值为true的时候,从键盘读取信息,当参数值为false的时候,从纸带读入机读取信息,伪代码如程序2所示。
// 程序2,源码program2包。public class Copy { ReadKeyboard rdKbd = new ReadKeyboard();//从键盘读取字符 WritePrinter wrtPrt = new WritePrinter();//将字符写入打印机 ReadPapertape rdPt = new ReadPapertape();//从纸带读入机读取字符 /** * copy方法增加一个boolean类型的入参ptFlag,来对输入字符的设备做判断。 * 当ptFlag为true的时候,表示从键盘读取字符,为false的时候,表示从纸带 * 读入机读取字符。 * @param ptFlag */ public void copy(boolean ptFlag){ char c; while(( c = ptFlag ? rdKbd.read() : rdPt.read()) != EOF){ wrtPrt.write(c); } }}/* * 增加一个类,表示从纸带读入机读取字符 */public class ReadPapertape { public char read(){ char c = 'p';// 'p'是从纸带读入机读取到的字符 System.out.println("从带读入机读取字符"); return c; }}
项目再次上线了,运行一段时间之后,业务方又提出来新的需求,希望这个程序不仅支持将字符写入到打印机中,还可以写入到纸带穿孔机上。要解决这个问题,唯一的解决方案就是修改Copy类,在copy方法中再加一个boolean类型的入参,当参数为true的时候,将读取到的字符写入到打印机中,当参数为false的时候,将读取到的字符写入到纸带穿孔机上,伪代码如程序3所示。
// 程序3,源码program3包。// 主程序public class Copy { ReadKeyboard rdKbd = new ReadKeyboard();//从键盘读取字符 WritePrinter wrtPrt = new WritePrinter();//将字符写入打印机 ReadPapertape rdPt = new ReadPapertape();//从纸带读入机读取字符 WriteTapepuncher wrtTpc = new WriteTapepuncher();//将字符写入纸带穿孔机 /** * copy方法再增加一个boolean类型的入参punchFlag,来对输出字符的设备做判断。 * 当punchFlag为true的时候,表示将字符写入打印机,为false的时候,表示将字 * 符写入纸带穿孔机。 * @param ptFlag * @param punchFlag */ public void copy(boolean ptFlag, boolean punchFlag){ char c; while(( c = ptFlag ? rdKbd.read() : rdPt.read()) != EOF){ if (punchFlag) { wrtTpc.write(c); } else { wrtPrt.write(c); } } }}// 增加一个类,表示将读取到的字符写入纸带穿孔机public class WriteTapepuncher { public void write(char c){ System.out.println("将字符写入纸带穿孔机"); }}// 客户端public class Client { public static void main(String[] args) { Copy copy = new Copy(); /** * 第一个参数传true,表示从键盘读取字符, * 第二个参数传false,表示将字符写入到纸带穿孔机。 */ copy.copy(true,false); }}
程序运行结果
从键盘读取字符
将字符写入打印机
你以为程序到这里就定型了吗,那是不可能滴,业务方还是会不断提出新的需求,然后迫使你不断修改程序,任何输入输出设备的再次变更都会迫使你对while循环的条件判断和while体中的if语句进行彻底的重新组织,此时的设计已经表现出了僵化性、脆弱性的症状,这种趋势继续下去,程序将会变得混乱不堪,越到后面程序越难改动,最后程序可能就废掉了,没人愿意接手这个烂摊子,只能重新启动另一个项目。
使用策略模式对系统进行重构,使其遵循依赖倒置原则
要想解决上述由于需求不断变更导致软件僵化的情况,我们可以使用敏捷开发方法,在需求的变更中不断完善系统的设计。使用敏捷开发方法时,一开始编写的代码和程序1完全一样(没有人能够做到在业务方第一次提出需求的时候,就设计出完美的设计),当业务要求可以从纸带读入机读取字符时,敏捷开发人员一般会做出如下反应:修改设计并且使修改后的设计对于无论哪一类需求的变化都具有弹性,此时程序2就变成了程序4,后续我们会讲到这里修改设计用到的是23种设计模中的策略模式,大家现在先只看这种重构带来的好处,后续连载策略模式的时候,再回头看这部分,就会有更加清晰的认识。
UML设计如如图2,伪代码如程序4。
// 程序4,源码program3包。// 主程序public class Copy { public void copy(Reader reader, Writer writer){ char c; while((c = reader.read()) != EOF){ writer.write(c); } }}// 输入设备抽象public interface Reader { char read();}// 输出设备抽象public interface Writer { void write(char c);}// 输入设备是键盘public class KeyboardReader implements Reader{ public char read(){ char c = 'k';// 'k'是从键盘读取到的字符; System.out.println("从键盘读取字符"); return c; }}// 输出设备是打印机public class PrinterWriter implements Writer{ public void write(char c){ System.out.println("将字符写入打印机"); }}// 客户端public class Client { public static void main(String[] args) { Copy copy = new Copy(); // 我们希望从键盘读取字符,将字符输入到打印机中 copy.copy(new KeyboardReader(),new PrinterWriter()); }}
程序运行结果:
从键盘读取字符
将字符写入打印机
当我们想增加新的输入输出设备时,只需要增加Reader和Writer接口的实现类,然后客户端Client中调用Copy类的copy方法中传入你希望的输入输出设备即可。只需要增加类,不需要修改现有的Copy类,就可以应对需求的不断变更,此时的程序就是灵活的,是完全满足开闭原则的。
总结:图1的依赖箭头都是从上往下的,图2的箭头出现了从下往上的情况,依赖关系确实倒置了。另外,此例子也很好的解释了高层模块不应该依赖低层模块,二者都应该依赖于抽象这句话。在最开始的设计中,高层模块Copy直接依赖低层模块,即具体的输入输出设备(ReadKeyboard、WritePrinter类)。调整设计后,高层模块和低层模块都依赖于抽象的输入输出设备(Reader和Writer接口)。
(3) 如何使我们设计的程序满足依赖倒置原则呢?
依赖倒置原则基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定地多,以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在java语言中,抽象指的是接口或抽象类,细节指的是具体的实现类。
要想让我们的程序满足依赖倒置原则,我们要面向接口编程,而不是面向实现编程。
什么是面向接口编程?我个人的理解是,在系统分析和架构中,分清层次和依赖关系,每个层次不是直接向其上层提供服务(即不是直接实例化在上层中),而是通过定义一组接口,向上层暴露其接口功能,上层对于下层仅仅是接口依赖,而不依赖具体类。
这样做的好处是,大大提高了系统的灵活性,接口隔离了程序的上层和下层,当下层需要改变时,只要接口不改变,上层就不需要做任何修改。
注意:面向接口编程中的接口和面向对象语言中的接口(比如java语言中的interface)是不一样的,面向接口编程中的"接口"二字应该比单纯面向对象语言中的接口范围更大。面向对象语言中的"接口"是指具体的一种代码结构,"面向接口编程"中的"接口"可以说是一种从软件架构的角度、从一个更抽象的层面上指那种用于隐藏具体底层类和实现多态性的结构部件。
程序1、2、3,面向实现编程。在Copy类中直接调用ReadKeyboard、WritePrinter等实现类。
程序4,面向接口编程。在Copy类中调用Reader、Writer接口,ReadKeyboard、WritePrinter类实现Reader、Writer接口,根据场景不同,我们在Client中就创建不同类的实例并将其传给Copy类,根据多态特性中的向上转型,Copy类会调用我们传给它的实现类。总之一句话,需要啥传啥,后续增加的类只要实现Reader、Writer接口,依然可以按照上述方法进行。
(4) 总结
依赖倒置原则的本质就是通过抽象(抽象类或接口)使各个类或模块实现彼此独立,不互相影响,实现模块间的松耦合。在项目中使用这个规则需要以下原则:
- 每个类尽量都有接口或者抽象类,或者抽象类和接口两都具备
- 变量的表面类型尽量是接口或者抽象类
- 任何类都不应该从具体类派生
- 尽量不要重写基类已经写好的方法(里式替换原则),依赖倒置原则结合里式替换原则来使用。
一句话:依赖倒置原则的核心就是面向抽象(抽象类或者接口)编程,或者叫面向接口编程。
源码位置:
https://github.com/52Hzhaha/designPrinciple
参考书籍:《敏捷软件开发:原则、模式与实践》
最后
以上就是诚心日记本为你收集整理的依赖倒置原则_面向对象设计原则之依赖倒置原则(DIP)的全部内容,希望文章能够帮你解决依赖倒置原则_面向对象设计原则之依赖倒置原则(DIP)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复