面向对象设计原则之依赖倒转原则
依赖倒转原则(Dependency Inversion Principle,简称DIP)是指将两个模块之间的依赖关系倒置为依赖抽象类或接口。具体有两层含义:
-
高层模块不应该依赖于低层模块,二者都应该依赖于抽象;
-
抽象不应该依赖于细节,细节应该依赖于抽象。
依赖倒转(倒置)的中心思想是面向接口编程。
依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在 java 中,抽象指的是接口或抽象类,细节就是具体的实现类。
使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成
所谓依赖是指如果一个模块A使用另一个模块B,我们称模块A依赖模块B。在应用程序中,有一些低层次的类,这些类实现了一些基本的或初级的操作,我们称之为低层模块;另外,有一些高层次的类,这些类封装了某些复杂的逻辑,这些类我们称之为高层模块。高层次模块要完成自己封装的功能,就必须要使用低层模块,于是高层模块就依赖于低层模块。
高层模块依赖于低层模块的现象,在传统的结构化程序设计中,是非常常见的。因为结构化程序设计就是采用由上到下、逐层分解的策略,把大型和复杂的软件系统分解成若干个人们易于理解和易于分析的子系统。这里的分解是根据软件系统的逻辑特性和系统内部各成分之间的逻辑关系进行的。在分解过程中,被分解的上层就是下层的抽象,下层为上层的具体细节。这样,就造成高层抽象模块依赖低层模块;抽象层依赖具体层。但在实际系统中,抽象层是相对稳定的,而低层模块却是经常变动的。因为高层模块依赖于低层模块,一旦低层模块发生改变,高层模块也会受到影响。为了保持系统的稳定,应该使低层模块依赖于高层模块。因此,结构化程序设计的方法是不正确的。
那么,如何让低层模块依赖于高层模块呢?我们知道,高层模块肯定要使用低层模块提供的服务,不可能不让二者之间完全不存在依赖关系。但是,在面向对象设计中,类和类之间依赖关系可以分为两种类型:
- 具体耦合关系:发生在两个具体的(可实例化的)类之间,经由一个类对另一个具体类的直接引用造成。
- 抽象耦合关系:发生在一个具体类和一个抽象类(或接口)之间,使两个必须发生关系的类之间存有最大的灵活性。
如果高层模块直接调用低层模块提供的服务,那么就是具体耦合关系,这样高层模块依赖于低层模块就不可避免。但是,如果我们使用抽象耦合关系,在高层模块和低层模块之间定义一个抽象接口,高层模块调用抽象接口定义的方法,低层模块实现该接口。这样,就消除了高层模块和低层模块之间的直接依赖关系。现在,高层模块就不依赖于低层模块了,二者都依赖于抽象。同时也实现了“抽象不应该依赖于细节,细节应该依赖于抽象”。
依赖倒转原则的本质就是要求将类之间的关系建立在抽象接口的基础上的。通过上面的方式,将错误的依赖关系倒转过来,使具体实现类依赖于抽象类和接口。这就是依赖倒转原则中“倒转”的由来。
以抽象方式耦合是依赖倒转原则的关键。抽象耦合关系总要涉及具体类从抽象类继承,并且需要保证在任何引用到基类的地方都可以改换成其子类,因此,里氏替换原则是依赖倒转原则的基础。
依赖倒转原则带来的一个启示是:针对接口编程,而不是针对实现编程。也就是说,当客户要使用一个接口的实现类功能时,应该针对定义这些功能的接口编程,而不是针对该接口的实现类编程。
传统的调用方法
package com.study; public class DependencyInversion { public static void main(String[] args) { Person person = new Person(); person.receive(new Email()); } } class Email { public String getInfo(){ return "电子邮件信息: hello,world"; } } /** * 1.优点:调用方式简单 * 2.存在问题:如果我们获取的对象是 微信,短信等等,则新增类,同时 Person 也要增加相应的接收方法 * 3.解决方法:引入一个抽象的接口 IReceiver, 表示接收者, 这样 Person 类与接口 IReceiver 发生依赖 * 因为 Email, WeiXin 等等属于接收的范围,他们各自实现 IReceiver 接口, 这样我们就符号依赖倒转原则 */ class Person { public void receive(Email email){ System.out.println(email.getInfo()); } }
通过依赖倒转的案例
package com.study; public class DependencyInversion { public static void main(String[] args) { Person person = new Person(); person.receive(new Email()); person.receive(new WX()); } } interface IReceive { String getInfo(); } class Email implements IReceive{ public String getInfo(){ return "电子邮件信息: hello,world"; } } //添加微信类 class WX implements IReceive{ public String getInfo(){ return "微信信息: hello,world"; } } class Person { public void receive(IReceive iReceive){ System.out.println(iReceive.getInfo()); } }
再比如在java应用中使用logger框架有很多选择,什么log4j,logback,common logging等,每个logger的API和用法都稍有不同,有的需要用isLoggable()来进行预判断以便提高性能,有的则不需要。对于要切换不同的logger框架的情形,就更是头疼了,有可能要改动很多地方。产生这些不便的原因是我们直接依赖了logger框架,应用和框架的耦合性很高。
怎么破? 遵循下依赖倒置原则就能很容易解决,依赖倒置就是你不要直接依赖我,你和我都同时依赖一个接口(所以有时候也叫面向接口的编程),这样我们之间就解耦了,依赖和被依赖方都可以自由改动了。
依赖关系传递的三种方式
接口传递,构造方法传递,setter 方式传递
依赖倒转原则的注意事项和细节
-
低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好
-
变量的
声明类型尽量是抽象类或接口
, 这样我们的变量引用和实际对象间,就存在一个缓冲层
,利于程序扩展和优化 -
继承时遵循
里氏替换原则
摘自:https://mp.weixin.qq.com/s/S8MoamWKdichF0cKXqAfYg
面向过程
是的,不管你承认与否,很多时候,我们都是操着面向对象的语言干着面向过程的勾当。面向对象不仅是一个语言,更是一种思维方式。在我们追逐云计算、深度学习、区块链这些技术热点的时候,静下心来问问自己我们是不是真的掌握了OOD;在我们强调工程师要具备业务Sense,产品Sense,数据Sense,算法Sense,XXSense的时候,是不是忽略了对工程能力的要求。
据我观察大部分工程师(包括我自己)的OO能力还远没有达到精通的程度,这种OO思想的缺乏主要体现在两个方面,一个是很多同学不了解SOLID原则,不懂设计模式,不会画UML图,或者只是知道,但从来不会运用到实践中;另一个是不会进行领域建模,关于领域建模争论已经很多了,我的观点是DDD很好,但不是银弹,用和不用取决于场景。但不管怎样,请你抛开偏见,好好的研读一下EricEvans的《领域驱动设计》,如果有认知升级的感悟,恭喜你,你进阶了。
我个人认为DDD最大的好处是将业务语义显现化,把原先晦涩难懂的业务算法逻辑,通过领域对象(Domain Object),统一语言(Ubiquitous Language)将领域概念清晰的显性化表达出来。相信我,这种表达带来的代码可读性的提升,会让接手你代码的人对你心怀感恩的。
面向对象
领域建模
准确的说DDD不是一个架构,而是思想和方法论。所以在架构层面我们并没有强制约束要使用DDD,但对于像我们这样的复杂业务场景,我们强烈建议使用DDD代替事务脚本(TS: Transaction Script)。因为TS的贫血模式,里面只有数据结构,完全没有对象(数据+行为)的概念,这也是为什么我们叫它是面向过程的原因。然而DDD是面向对象的,是一种知识丰富的设计(Knowledge Rich Design),怎么理解?,就是通过领域对象(Domain Object),领域语言(Ubiquitous Language)将核心的领域概念通过代码的形式表达出来,从而增加代码的可理解性。这里的领域核心不仅仅是业务里的“名词”,所有的业务活动和规则如同实体一样,都需要明确的表达出来。